1
0
mirror of https://github.com/bitwarden/directory-connector synced 2026-02-24 16:43:06 +00:00

Compare commits

..

6 Commits

Author SHA1 Message Date
Jared McCannon
a8da8be44e updating package lock 2026-01-08 09:32:35 -06:00
Jared McCannon
6e258cb9f6 Merge branch 'main' into jared/typescript-upgrade
# Conflicts:
#	package-lock.json
#	package.json
2026-01-08 09:32:07 -06:00
Jared McCannon
b813d14d85 updating package-lock 2026-01-06 10:08:39 -06:00
Jared McCannon
233d72b4d1 corrected tests to pull the new type 2026-01-06 10:06:10 -06:00
Jared McCannon
7508af725b fixed build errors 2026-01-06 09:36:17 -06:00
renovate[bot]
eef12c65fe [deps]: Update typescript to v5.9.3 2025-12-31 16:02:10 +00:00
55 changed files with 8524 additions and 9229 deletions

View File

@@ -9,3 +9,26 @@
## 📸 Screenshots
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
## ⏰ Reminders before review
- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
## 🦮 Reviewer guidelines
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes

View File

@@ -23,7 +23,7 @@ jobs:
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -51,36 +51,42 @@ jobs:
contents: read
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Set up system dependencies
- name: Update NPM
run: |
sudo apt-get update
sudo apt-get -y install libdbus-1-dev libsecret-1-dev pkg-config
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
- 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" "node_modules/dc-native/dc_native.linux-x64-gnu.node"
run: zip -j "dist-cli/bwdc-linux-$_PACKAGE_VERSION.zip" "dist-cli/linux/bwdc" "keytar/linux/build/Release/keytar.node"
- name: Version Test
run: |
@@ -123,31 +129,42 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- 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: 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" "node_modules/dc-native/dc_native.darwin-x64.node"
run: zip -j "dist-cli/bwdc-macos-$_PACKAGE_VERSION.zip" "dist-cli/macos/bwdc" "keytar/macos/build/Release/keytar.node"
- name: Version Test
run: |
@@ -183,7 +200,7 @@ jobs:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -192,29 +209,42 @@ jobs:
choco install checksum --no-progress
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- 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: 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 .\node_modules\dc-native\dc_native.win32-x64-msvc.node
run: 7z a .\dist-cli\bwdc-windows-%_PACKAGE_VERSION%.zip .\dist-cli\windows\bwdc.exe .\keytar\windows\keytar.node
- name: Version Test
shell: pwsh
@@ -249,21 +279,21 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install $(node -v)
- name: Print environment
run: |
@@ -349,24 +379,26 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Set up environment
run: |
sudo apt-get update
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev libdbus-1-dev
sudo apt-get -y install pkg-config libxss-dev libsecret-1-dev
sudo apt-get -y install rpm
- name: NPM Install
@@ -407,19 +439,21 @@ jobs:
HUSKY: 0
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
- name: Update NPM
run: |
npm install -g node-gyp
node-gyp install "$(node -v)"
- name: Print environment
run: |

View File

@@ -40,7 +40,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -52,7 +52,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -129,7 +129,7 @@ jobs:
- name: Report test results
id: report
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
@@ -143,6 +143,4 @@ jobs:
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Upload results to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1

View File

@@ -1,46 +0,0 @@
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

View File

@@ -26,7 +26,7 @@ jobs:
release_version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false

View File

@@ -22,7 +22,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
@@ -34,7 +34,7 @@ jobs:
echo "node_version=$NODE_VERSION" >> "$GITHUB_OUTPUT"
- name: Set up Node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
@@ -53,7 +53,7 @@ jobs:
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@b082adf0eced0765477756c2a610396589b8c637 # v2.5.0
uses: dorny/test-reporter@fe45e9537387dac839af0d33ba56eed8e24189e8 # v2.3.0
# This will skip the job if it's a pull request from a fork, because that won't have permission to upload test results.
# PRs from the repository and all other events are OK.
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository) && !cancelled()
@@ -67,6 +67,4 @@ jobs:
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
- name: Upload results to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
with:
report_type: test_results
uses: codecov/test-results-action@0fa95f0e1eeaafde2c782583b36b28ad0d8c77d3 # v1.2.1

View File

@@ -50,7 +50,7 @@ jobs:
permission-contents: write
- name: Checkout Branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
token: ${{ steps.app-token.outputs.token }}
persist-credentials: true

4
.gitignore vendored
View File

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

2
.nvmrc
View File

@@ -1 +1 @@
v22
v20

View File

@@ -18,17 +18,15 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": {
"base": "dist"
},
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "tsconfig.json",
"assets": [],
"styles": [],
"scripts": [],
"browser": "src/main.ts"
"scripts": []
}
}
}

View File

@@ -11,7 +11,6 @@
"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,7 +23,6 @@ export default [
"eslint.config.mjs",
"scripts/**/*.js",
"**/node_modules/**",
"native/**",
],
},

View File

@@ -1,77 +1,75 @@
import { animate, state, style, transition, trigger } from "@angular/animations";
import { CommonModule } from "@angular/common";
import { Component, ModuleWithProviders, NgModule } from "@angular/core";
import { DefaultNoComponentGlobalConfig, GlobalConfig, Toast, TOAST_CONFIG } from "ngx-toastr";
import {
DefaultNoComponentGlobalConfig,
GlobalConfig,
Toast as BaseToast,
ToastPackage,
ToastrService,
TOAST_CONFIG,
} from "ngx-toastr";
@Component({
selector: "[toast-component2]",
template: `
@if (options().closeButton) {
<button (click)="remove()" type="button" class="toast-close-button" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
}
<button
*ngIf="options.closeButton"
(click)="remove()"
type="button"
class="toast-close-button"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
<div class="icon">
<i></i>
</div>
<div>
@if (title()) {
<div [class]="options().titleClass" [attr.aria-label]="title()">
{{ title() }}
@if (duplicatesCount) {
[{{ duplicatesCount + 1 }}]
}
</div>
}
@if (message() && options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[innerHTML]="message()"
></div>
}
@if (message() && !options().enableHtml) {
<div
role="alertdialog"
aria-live="polite"
[class]="options().messageClass"
[attr.aria-label]="message()"
>
{{ message() }}
</div>
}
</div>
@if (options().progressBar) {
<div>
<div class="toast-progress" [style.width]="width + '%'"></div>
<div *ngIf="title" [class]="options.titleClass" [attr.aria-label]="title">
{{ title }} <ng-container *ngIf="duplicatesCount">[{{ duplicatesCount + 1 }}]</ng-container>
</div>
}
`,
styles: `
:host {
&.toast-in {
animation: toast-animation var(--animation-duration) var(--animation-easing);
}
&.toast-out {
animation: toast-animation var(--animation-duration) var(--animation-easing) reverse
forwards;
}
}
@keyframes toast-animation {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<div
*ngIf="message && options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[innerHTML]="message"
></div>
<div
*ngIf="message && !options.enableHtml"
role="alertdialog"
aria-live="polite"
[class]="options.messageClass"
[attr.aria-label]="message"
>
{{ message }}
</div>
</div>
<div *ngIf="options.progressBar">
<div class="toast-progress" [style.width]="width + '%'"></div>
</div>
`,
animations: [
trigger("flyInOut", [
state("inactive", style({ opacity: 0 })),
state("active", style({ opacity: 1 })),
state("removed", style({ opacity: 0 })),
transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")),
transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")),
]),
],
preserveWhitespaces: false,
standalone: false,
})
export class BitwardenToast extends Toast {}
export class BitwardenToast extends BaseToast {
constructor(
protected toastrService: ToastrService,
public toastPackage: ToastPackage,
) {
super(toastrService, toastPackage);
}
}
export const BitwardenToastGlobalConfig: GlobalConfig = {
...DefaultNoComponentGlobalConfig,

View File

@@ -0,0 +1,195 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EncryptionType } from "@/jslib/common/src/enums/encryptionType";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { ContainerService } from "@/jslib/common/src/services/container.service";
describe("EncString", () => {
afterEach(() => {
(window as any).bitwardenContainerService = undefined;
});
describe("Rsa2048_OaepSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("3.data");
expect(encString).toEqual({
data: "data",
encryptedString: "3.data",
encryptionType: 3,
});
});
it("invalid", () => {
const encString = new EncString("3.data|test");
expect(encString).toEqual({
encryptedString: "3.data|test",
encryptionType: 3,
});
});
});
describe("decrypt", () => {
const encString = new EncString(EncryptionType.Rsa2048_OaepSha256_B64, "data");
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).resolves("decrypted");
beforeEach(() => {
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
});
it("decrypts correctly", async () => {
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("decrypted");
});
it("result should be cached", async () => {
const decrypted = await encString.decrypt(null);
cryptoService.received(1).decryptToUtf8(Arg.any(), Arg.any());
expect(decrypted).toBe("decrypted");
});
});
});
describe("AesCbc256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_B64, "data", "iv");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
describe("parse existing", () => {
it("valid", () => {
const encString = new EncString("0.iv|data");
expect(encString).toEqual({
data: "data",
encryptedString: "0.iv|data",
encryptionType: 0,
iv: "iv",
});
});
it("invalid", () => {
const encString = new EncString("0.iv|data|mac");
expect(encString).toEqual({
encryptedString: "0.iv|data|mac",
encryptionType: 0,
});
});
});
});
describe("AesCbc256_HmacSha256_B64", () => {
it("constructor", () => {
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("valid", () => {
const encString = new EncString("2.iv|data|mac");
expect(encString).toEqual({
data: "data",
encryptedString: "2.iv|data|mac",
encryptionType: 2,
iv: "iv",
mac: "mac",
});
});
it("invalid", () => {
const encString = new EncString("2.iv|data");
expect(encString).toEqual({
encryptedString: "2.iv|data",
encryptionType: 2,
});
});
});
it("Exit early if null", () => {
const encString = new EncString(null);
expect(encString).toEqual({
encryptedString: null,
});
});
describe("decrypt", () => {
it("throws exception when bitwarden container not initialized", async () => {
const encString = new EncString(null);
expect.assertions(1);
try {
await encString.decrypt(null);
} catch (e) {
expect(e.message).toEqual("global bitwardenContainerService not initialized.");
}
});
it("handles value it can't decrypt", async () => {
const encString = new EncString(null);
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
cryptoService.decryptToUtf8(encString, Arg.any()).throws("error");
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
const decrypted = await encString.decrypt(null);
expect(decrypted).toBe("[error: cannot decrypt]");
expect(encString).toEqual({
decryptedValue: "[error: cannot decrypt]",
encryptedString: null,
});
});
it("passes along key", async () => {
const encString = new EncString(null);
const key = Substitute.for<SymmetricCryptoKey>();
const cryptoService = Substitute.for<CryptoService>();
cryptoService.getOrgKey(null).resolves(null);
(window as any).bitwardenContainerService = new ContainerService(cryptoService);
await encString.decrypt(null, key);
cryptoService.received().decryptToUtf8(encString, key);
});
});
});

View File

@@ -9,19 +9,19 @@ describe("SymmetricCryptoKey", () => {
new SymmetricCryptoKey(null);
};
expect(t).toThrow("Must provide key");
expect(t).toThrowError("Must provide key");
});
describe("guesses encKey from key length", () => {
it("AesCbc256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key);
const cryptoKey = new SymmetricCryptoKey(key.buffer as ArrayBuffer);
expect(cryptoKey).toEqual({
encKey: key,
encKey: key.buffer,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 0,
key: key,
key: key.buffer,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
});
@@ -29,41 +29,41 @@ describe("SymmetricCryptoKey", () => {
it("AesCbc128_HmacSha256_B64", () => {
const key = makeStaticByteArray(32);
const cryptoKey = new SymmetricCryptoKey(key, EncryptionType.AesCbc128_HmacSha256_B64);
const cryptoKey = new SymmetricCryptoKey(key.buffer as ArrayBuffer, EncryptionType.AesCbc128_HmacSha256_B64);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 16),
encKey: key.buffer.slice(0, 16),
encKeyB64: "AAECAwQFBgcICQoLDA0ODw==",
encType: 1,
key: key,
key: key.buffer,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: key.slice(16, 32),
macKey: key.buffer.slice(16, 32),
macKeyB64: "EBESExQVFhcYGRobHB0eHw==",
});
});
it("AesCbc256_HmacSha256_B64", () => {
const key = makeStaticByteArray(64);
const cryptoKey = new SymmetricCryptoKey(key);
const cryptoKey = new SymmetricCryptoKey(key.buffer as ArrayBuffer);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKey: key.buffer.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: 2,
key: key,
key: key.buffer,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKey: key.buffer.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
});
});
it("unknown length", () => {
const t = () => {
new SymmetricCryptoKey(makeStaticByteArray(30));
new SymmetricCryptoKey(makeStaticByteArray(30).buffer as ArrayBuffer);
};
expect(t).toThrow("Unable to determine encType.");
expect(t).toThrowError("Unable to determine encType.");
});
});
});

View File

@@ -0,0 +1,84 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { StorageService } from "@/jslib/common/src/abstractions/storage.service";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
const userId = "USER_ID";
describe("State Migration Service", () => {
let storageService: SubstituteOf<StorageService>;
let secureStorageService: SubstituteOf<StorageService>;
let stateFactory: SubstituteOf<StateFactory>;
let stateMigrationService: StateMigrationService;
beforeEach(() => {
storageService = Substitute.for<StorageService>();
secureStorageService = Substitute.for<StorageService>();
stateFactory = Substitute.for<StateFactory>();
stateMigrationService = new StateMigrationService(
storageService,
secureStorageService,
stateFactory,
);
});
describe("StateVersion 3 to 4 migration", async () => {
beforeEach(() => {
const globalVersion3: Partial<GlobalState> = {
stateVersion: StateVersion.Three,
};
storageService.get("global", Arg.any()).resolves(globalVersion3);
storageService.get("authenticatedAccounts", Arg.any()).resolves([userId]);
});
it("clears everBeenUnlocked", async () => {
const accountVersion3: Account = {
profile: {
apiKeyClientId: null,
convertAccountToKeyConnector: null,
email: "EMAIL",
emailVerified: true,
everBeenUnlocked: true,
hasPremiumPersonally: false,
kdfIterations: 100000,
kdfType: 0,
keyHash: "KEY_HASH",
lastSync: "LAST_SYNC",
userId: userId,
usesKeyConnector: false,
forcePasswordReset: false,
},
};
const expectedAccountVersion4: Account = {
profile: {
...accountVersion3.profile,
},
};
delete expectedAccountVersion4.profile.everBeenUnlocked;
storageService.get(userId, Arg.any()).resolves(accountVersion3);
await stateMigrationService.migrate();
storageService.received(1).save(userId, expectedAccountVersion4, Arg.any());
});
it("updates StateVersion number", async () => {
await stateMigrationService.migrate();
storageService.received(1).save(
"global",
Arg.is((globals: GlobalState) => globals.stateVersion === StateVersion.Four),
Arg.any(),
);
});
});
});

View File

@@ -1,3 +1,7 @@
import { Substitute, Arg } from "@fluffy-spoon/substitute";
import { EncString } from "@/jslib/common/src/models/domain/encString";
function newGuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
@@ -17,10 +21,17 @@ export function BuildTestObject<T, K extends keyof T = keyof T>(
return Object.assign(constructor === null ? {} : new constructor(), def) as T;
}
export function mockEnc(s: string): EncString {
const mock = Substitute.for<EncString>();
mock.decrypt(Arg.any(), Arg.any()).resolves(s);
return mock;
}
export function makeStaticByteArray(length: number, start = 0) {
const arr = new Uint8Array(length);
for (let i = 0; i < length; i++) {
arr[i] = start + i;
}
return arr.buffer;
return arr;
}

View File

@@ -3,6 +3,5 @@ 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
Five = 5, // Migrate Windows keychain credentials from keytar (UTF-8) to desktop_core (UTF-16)
Latest = Five,
Latest = Four,
}

View File

@@ -26,4 +26,9 @@ export class NodeUtils {
.on("error", (err) => reject(err));
});
}
// https://stackoverflow.com/a/31394257
static bufferToArrayBuffer(buf: Buffer): ArrayBuffer {
return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer;
}
}

View File

@@ -36,7 +36,7 @@ export class Utils {
Utils.global = Utils.isNode && !Utils.isBrowser ? global : window;
}
static fromB64ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromB64ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "base64"));
} else {
@@ -49,11 +49,11 @@ export class Utils {
}
}
static fromUrlB64ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromUrlB64ToArray(str: string): Uint8Array {
return Utils.fromB64ToArray(Utils.fromUrlB64ToB64(str));
}
static fromHexToArray(str: string): Uint8Array<ArrayBuffer> {
static fromHexToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "hex"));
} else {
@@ -65,7 +65,7 @@ export class Utils {
}
}
static fromUtf8ToArray(str: string): Uint8Array<ArrayBuffer> {
static fromUtf8ToArray(str: string): Uint8Array {
if (Utils.isNode) {
return new Uint8Array(Buffer.from(str, "utf8"));
} else {
@@ -78,7 +78,7 @@ export class Utils {
}
}
static fromByteStringToArray(str: string): Uint8Array<ArrayBuffer> {
static fromByteStringToArray(str: string): Uint8Array {
const arr = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
arr[i] = str.charCodeAt(i);
@@ -99,8 +99,8 @@ export class Utils {
}
}
static fromBufferToUrlB64(buffer: Uint8Array<ArrayBuffer>): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer.buffer));
static fromBufferToUrlB64(buffer: ArrayBuffer): string {
return Utils.fromB64toUrlB64(Utils.fromBufferToB64(buffer));
}
static fromB64toUrlB64(b64Str: string) {
@@ -164,7 +164,7 @@ export class Utils {
}
static fromUtf8ToUrlB64(utfStr: string): string {
return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr));
return Utils.fromBufferToUrlB64(Utils.fromUtf8ToArray(utfStr).buffer as ArrayBuffer);
}
static fromB64ToUtf8(b64Str: string): string {

View File

@@ -42,9 +42,9 @@ export class ChallengeResponse extends BaseResponse implements PublicKeyCredenti
super(response);
this.attestation = this.getResponseProperty("attestation");
this.authenticatorSelection = this.getResponseProperty("authenticatorSelection");
this.challenge = Utils.fromUrlB64ToArray(this.getResponseProperty("challenge"));
this.challenge = Utils.fromUrlB64ToArray(this.getResponseProperty("challenge")) as Uint8Array<ArrayBuffer>;
this.excludeCredentials = this.getResponseProperty("excludeCredentials").map((c: any) => {
c.id = Utils.fromUrlB64ToArray(c.id).buffer;
c.id = Utils.fromUrlB64ToArray(c.id).buffer as ArrayBuffer;
return c;
});
this.extensions = this.getResponseProperty("extensions");

View File

@@ -109,7 +109,7 @@ export class CryptoService implements CryptoServiceAbstraction {
): Promise<SymmetricCryptoKey> {
const key = await this.retrieveKeyFromStorage(keySuffix, userId);
if (key != null) {
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer);
const symmetricKey = new SymmetricCryptoKey(Utils.fromB64ToArray(key).buffer as ArrayBuffer);
if (!(await this.validateKey(symmetricKey))) {
this.logService.warning("Wrong key, throwing away stored key");
@@ -512,7 +512,7 @@ export class CryptoService implements CryptoServiceAbstraction {
let plainBuf: ArrayBuffer;
if (typeof plainValue === "string") {
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer;
plainBuf = Utils.fromUtf8ToArray(plainValue).buffer as ArrayBuffer;
} else {
plainBuf = plainValue;
}
@@ -539,7 +539,7 @@ export class CryptoService implements CryptoServiceAbstraction {
}
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
return new EncArrayBuffer(encBytes.buffer);
return new EncArrayBuffer(encBytes.buffer as ArrayBuffer);
}
async rsaEncrypt(data: ArrayBuffer, publicKey?: ArrayBuffer): Promise<EncString> {
@@ -585,7 +585,7 @@ export class CryptoService implements CryptoServiceAbstraction {
throw new Error("encPieces unavailable.");
}
const data = Utils.fromB64ToArray(encPieces[0]).buffer;
const data = Utils.fromB64ToArray(encPieces[0]).buffer as ArrayBuffer;
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
if (privateKey == null) {
throw new Error("No private key.");
@@ -608,9 +608,9 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async decryptToBytes(encString: EncString, key?: SymmetricCryptoKey): Promise<ArrayBuffer> {
const iv = Utils.fromB64ToArray(encString.iv).buffer;
const data = Utils.fromB64ToArray(encString.data).buffer;
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer : null;
const iv = Utils.fromB64ToArray(encString.iv).buffer as ArrayBuffer;
const data = Utils.fromB64ToArray(encString.data).buffer as ArrayBuffer;
const mac = encString.mac ? Utils.fromB64ToArray(encString.mac).buffer as ArrayBuffer : null;
const decipher = await this.aesDecryptToBytes(encString.encryptionType, data, iv, mac, key);
if (decipher == null) {
return null;
@@ -636,9 +636,9 @@ export class CryptoService implements CryptoServiceAbstraction {
const encBytes = new Uint8Array(encBuf);
const encType = encBytes[0];
let ctBytes: Uint8Array<ArrayBuffer> = null;
let ivBytes: Uint8Array<ArrayBuffer> = null;
let macBytes: Uint8Array<ArrayBuffer> = null;
let ctBytes: Uint8Array = null;
let ivBytes: Uint8Array = null;
let macBytes: Uint8Array = null;
switch (encType) {
case EncryptionType.AesCbc128_HmacSha256_B64:
@@ -667,9 +667,9 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.aesDecryptToBytes(
encType,
ctBytes.buffer,
ivBytes.buffer,
macBytes != null ? macBytes.buffer : null,
ctBytes.buffer as ArrayBuffer,
ivBytes.buffer as ArrayBuffer,
macBytes != null ? macBytes.buffer as ArrayBuffer : null,
key,
);
}
@@ -766,7 +766,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const macData = new Uint8Array(obj.iv.byteLength + obj.data.byteLength);
macData.set(new Uint8Array(obj.iv), 0);
macData.set(new Uint8Array(obj.data), obj.iv.byteLength);
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer, obj.key.macKey, "sha256");
obj.mac = await this.cryptoFunctionService.hmac(macData.buffer as ArrayBuffer, obj.key.macKey, "sha256");
}
return obj;
@@ -832,7 +832,7 @@ export class CryptoService implements CryptoServiceAbstraction {
macData.set(new Uint8Array(iv), 0);
macData.set(new Uint8Array(data), iv.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(
macData.buffer,
macData.buffer as ArrayBuffer,
theKey.macKey,
"sha256",
);
@@ -889,7 +889,7 @@ export class CryptoService implements CryptoServiceAbstraction {
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey.buffer);
return new SymmetricCryptoKey(newKey.buffer as ArrayBuffer);
}
private async hashPhrase(hash: ArrayBuffer, minimumEntropy = 64) {

View File

@@ -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("nativeSecureStorage", {
const val = ipcRenderer.sendSync("keytar", {
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("nativeSecureStorage", {
const val = ipcRenderer.sendSync("keytar", {
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("nativeSecureStorage", {
ipcRenderer.sendSync("keytar", {
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("nativeSecureStorage", {
ipcRenderer.sendSync("keytar", {
action: "deletePassword",
key: key,
keySuffix: options?.keySuffix ?? "",

View File

@@ -127,13 +127,6 @@ export class WindowMain {
},
});
// Enable SharedArrayBuffer. See https://developer.chrome.com/blog/enabling-shared-array-buffer/#cross-origin-isolation
this.win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
callback({ responseHeaders: details.responseHeaders });
});
if (this.windowStates[mainWindowSizeKey].isMaximized) {
this.win.maximize();
}

View File

@@ -94,7 +94,7 @@ describe("NodeCrypto Function Service", () => {
it("should fail with prk too small", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const f = cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(prk16Byte).buffer,
Utils.fromB64ToArray(prk16Byte).buffer as ArrayBuffer,
"info",
32,
"sha256",
@@ -105,7 +105,7 @@ describe("NodeCrypto Function Service", () => {
it("should fail with outputByteSize is too large", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const f = cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(prk32Byte).buffer,
Utils.fromB64ToArray(prk32Byte).buffer as ArrayBuffer,
"info",
8161,
"sha256",
@@ -170,9 +170,9 @@ describe("NodeCrypto Function Service", () => {
const key = makeStaticByteArray(32);
const data = Utils.fromUtf8ToArray("EncryptMe!");
const encValue = await nodeCryptoFunctionService.aesEncrypt(
data.buffer,
iv.buffer,
key.buffer,
data.buffer as ArrayBuffer,
iv.buffer as ArrayBuffer,
key.buffer as ArrayBuffer,
);
expect(Utils.fromBufferToB64(encValue)).toBe("ByUF8vhyX4ddU9gcooznwA==");
});
@@ -184,11 +184,11 @@ describe("NodeCrypto Function Service", () => {
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await nodeCryptoFunctionService.aesEncrypt(
data.buffer,
iv.buffer,
key.buffer,
data.buffer as ArrayBuffer,
iv.buffer as ArrayBuffer,
key.buffer as ArrayBuffer,
);
const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv.buffer, key.buffer);
const decValue = await nodeCryptoFunctionService.aesDecrypt(encValue, iv.buffer as ArrayBuffer, key.buffer as ArrayBuffer);
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
@@ -196,8 +196,8 @@ describe("NodeCrypto Function Service", () => {
describe("aesDecryptFast", () => {
it("should successfully decrypt data", async () => {
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer);
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer);
const iv = Utils.fromBufferToB64(makeStaticByteArray(16).buffer as ArrayBuffer);
const symKey = new SymmetricCryptoKey(makeStaticByteArray(32).buffer as ArrayBuffer);
const data = "ByUF8vhyX4ddU9gcooznwA==";
const params = nodeCryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey);
const decValue = await nodeCryptoFunctionService.aesDecryptFast(params);
@@ -212,9 +212,9 @@ describe("NodeCrypto Function Service", () => {
const key = makeStaticByteArray(32);
const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA==");
const decValue = await nodeCryptoFunctionService.aesDecrypt(
data.buffer,
iv.buffer,
key.buffer,
data.buffer as ArrayBuffer,
iv.buffer as ArrayBuffer,
key.buffer as ArrayBuffer,
);
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
});
@@ -228,11 +228,11 @@ describe("NodeCrypto Function Service", () => {
const value = "EncryptMe!";
const data = Utils.fromUtf8ToArray(value);
const encValue = await nodeCryptoFunctionService.rsaEncrypt(
data.buffer,
pubKey.buffer,
data.buffer as ArrayBuffer,
pubKey.buffer as ArrayBuffer,
"sha1",
);
const decValue = await nodeCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer, "sha1");
const decValue = await nodeCryptoFunctionService.rsaDecrypt(encValue, privKey.buffer as ArrayBuffer, "sha1");
expect(Utils.fromBufferToUtf8(decValue)).toBe(value);
});
});
@@ -248,8 +248,8 @@ describe("NodeCrypto Function Service", () => {
"/5jcercUtK2o+XrzNrL4UQ7yLZcFz6Bfwb/j6ICYvqd/YJwXNE6dwlL57OfwJyCdw2rRYf0/qI00t9u8Iitw==",
);
const decValue = await nodeCryptoFunctionService.rsaDecrypt(
data.buffer,
privKey.buffer,
data.buffer as ArrayBuffer,
privKey.buffer as ArrayBuffer,
"sha1",
);
expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!");
@@ -260,7 +260,7 @@ describe("NodeCrypto Function Service", () => {
it("should successfully extract key", async () => {
const nodeCryptoFunctionService = new NodeCryptoFunctionService();
const privKey = Utils.fromB64ToArray(RsaPrivateKey);
const publicKey = await nodeCryptoFunctionService.rsaExtractPublicKey(privKey.buffer);
const publicKey = await nodeCryptoFunctionService.rsaExtractPublicKey(privKey.buffer as ArrayBuffer);
expect(Utils.fromBufferToB64(publicKey)).toBe(RsaPublicKey);
});
});
@@ -326,8 +326,8 @@ function testPbkdf2(
it("should create valid " + algorithm + " key from array buffer input", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const key = await cryptoFunctionService.pbkdf2(
Utils.fromUtf8ToArray(regularPassword).buffer,
Utils.fromUtf8ToArray(regularEmail).buffer,
Utils.fromUtf8ToArray(regularPassword).buffer as ArrayBuffer,
Utils.fromUtf8ToArray(regularEmail).buffer as ArrayBuffer,
algorithm,
5000,
);
@@ -341,7 +341,7 @@ function testHkdf(
utf8Key: string,
unicodeKey: string,
) {
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer;
const ikm = Utils.fromB64ToArray("criAmKtfzxanbgea5/kelQ==").buffer as ArrayBuffer;
const regularSalt = "salt";
const utf8Salt = "üser_salt";
@@ -373,8 +373,8 @@ function testHkdf(
const cryptoFunctionService = new NodeCryptoFunctionService();
const key = await cryptoFunctionService.hkdf(
ikm,
Utils.fromUtf8ToArray(regularSalt).buffer,
Utils.fromUtf8ToArray(regularInfo).buffer,
Utils.fromUtf8ToArray(regularSalt).buffer as ArrayBuffer,
Utils.fromUtf8ToArray(regularInfo).buffer as ArrayBuffer,
32,
algorithm,
);
@@ -393,7 +393,7 @@ function testHkdfExpand(
it("should create valid " + algorithm + " " + outputByteSize + " byte okm", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const okm = await cryptoFunctionService.hkdfExpand(
Utils.fromB64ToArray(b64prk).buffer,
Utils.fromB64ToArray(b64prk).buffer as ArrayBuffer,
info,
outputByteSize,
algorithm,
@@ -433,7 +433,7 @@ function testHash(
it("should create valid " + algorithm + " hash from array buffer input", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const hash = await cryptoFunctionService.hash(
Utils.fromUtf8ToArray(regularValue).buffer,
Utils.fromUtf8ToArray(regularValue).buffer as ArrayBuffer,
algorithm,
);
expect(Utils.fromBufferToHex(hash)).toBe(regularHash);
@@ -443,8 +443,8 @@ function testHash(
function testHmac(algorithm: "sha1" | "sha256" | "sha512", mac: string, fast = false) {
it("should create valid " + algorithm + " hmac", async () => {
const cryptoFunctionService = new NodeCryptoFunctionService();
const value = Utils.fromUtf8ToArray("SignMe!!").buffer;
const key = Utils.fromUtf8ToArray("secretkey").buffer;
const value = Utils.fromUtf8ToArray("SignMe!!").buffer as ArrayBuffer;
const key = Utils.fromUtf8ToArray("secretkey").buffer as ArrayBuffer;
let computedMac: ArrayBuffer = null;
if (fast) {
computedMac = await cryptoFunctionService.hmacFast(value, key, algorithm);
@@ -462,8 +462,8 @@ function testCompare(fast = false) {
a[0] = 1;
a[1] = 2;
const equal = fast
? await cryptoFunctionService.compareFast(a.buffer, a.buffer)
: await cryptoFunctionService.compare(a.buffer, a.buffer);
? await cryptoFunctionService.compareFast(a.buffer as ArrayBuffer, a.buffer as ArrayBuffer)
: await cryptoFunctionService.compare(a.buffer as ArrayBuffer, a.buffer as ArrayBuffer);
expect(equal).toBe(true);
});
@@ -476,8 +476,8 @@ function testCompare(fast = false) {
b[0] = 3;
b[1] = 4;
const equal = fast
? await cryptoFunctionService.compareFast(a.buffer, b.buffer)
: await cryptoFunctionService.compare(a.buffer, b.buffer);
? await cryptoFunctionService.compareFast(a.buffer as ArrayBuffer, b.buffer as ArrayBuffer)
: await cryptoFunctionService.compare(a.buffer as ArrayBuffer, b.buffer as ArrayBuffer);
expect(equal).toBe(false);
});
@@ -489,8 +489,8 @@ function testCompare(fast = false) {
const b = new Uint8Array(2);
b[0] = 3;
const equal = fast
? await cryptoFunctionService.compareFast(a.buffer, b.buffer)
: await cryptoFunctionService.compare(a.buffer, b.buffer);
? await cryptoFunctionService.compareFast(a.buffer as ArrayBuffer, b.buffer as ArrayBuffer)
: await cryptoFunctionService.compare(a.buffer as ArrayBuffer, b.buffer as ArrayBuffer);
expect(equal).toBe(false);
});
}

View File

@@ -67,14 +67,14 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
t.set(previousT);
t.set(infoArr, previousT.length);
t.set([i + 1], t.length - 1);
previousT = new Uint8Array(await this.hmac(t.buffer, prk, algorithm));
previousT = new Uint8Array(await this.hmac(t.buffer as ArrayBuffer, prk, algorithm));
okm.set(previousT, runningOkmLength);
runningOkmLength += previousT.length;
if (runningOkmLength >= outputByteSize) {
break;
}
}
return okm.slice(0, outputByteSize).buffer;
return okm.slice(0, outputByteSize).buffer as ArrayBuffer;
}
hash(
@@ -147,19 +147,19 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
): DecryptParameters<ArrayBuffer> {
const p = new DecryptParameters<ArrayBuffer>();
p.encKey = key.encKey;
p.data = Utils.fromB64ToArray(data).buffer;
p.iv = Utils.fromB64ToArray(iv).buffer;
p.data = Utils.fromB64ToArray(data).buffer as ArrayBuffer;
p.iv = Utils.fromB64ToArray(iv).buffer as ArrayBuffer;
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
macData.set(new Uint8Array(p.iv), 0);
macData.set(new Uint8Array(p.data), p.iv.byteLength);
p.macData = macData.buffer;
p.macData = macData.buffer as ArrayBuffer;
if (key.macKey != null) {
p.macKey = key.macKey;
}
if (mac != null) {
p.mac = Utils.fromB64ToArray(mac).buffer;
p.mac = Utils.fromB64ToArray(mac).buffer as ArrayBuffer;
}
return p;
@@ -215,7 +215,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
const publicKeyAsn1 = forge.pki.publicKeyToAsn1(forgePublicKey);
const publicKeyByteString = forge.asn1.toDer(publicKeyAsn1).data;
const publicKeyArray = Utils.fromByteStringToArray(publicKeyByteString);
return Promise.resolve(publicKeyArray.buffer);
return Promise.resolve(publicKeyArray.buffer as ArrayBuffer);
}
async rsaGenerateKeyPair(length: 1024 | 2048 | 4096): Promise<[ArrayBuffer, ArrayBuffer]> {
@@ -241,7 +241,7 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
const privateKeyByteString = forge.asn1.toDer(privateKeyPkcs8).getBytes();
const privateKey = Utils.fromByteStringToArray(privateKeyByteString);
resolve([publicKey.buffer, privateKey.buffer]);
resolve([publicKey.buffer as ArrayBuffer, privateKey.buffer as ArrayBuffer]);
},
);
});
@@ -276,9 +276,9 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
private toArrayBuffer(value: Buffer | string | ArrayBuffer): ArrayBuffer {
let buf: ArrayBuffer;
if (typeof value === "string") {
buf = Utils.fromUtf8ToArray(value).buffer;
buf = Utils.fromUtf8ToArray(value).buffer as ArrayBuffer;
} else {
buf = new Uint8Array(value).buffer;
buf = new Uint8Array(value).buffer as ArrayBuffer;
}
return buf;
}

3498
native/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +0,0 @@
[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"

View File

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

34
native/index.d.ts vendored
View File

@@ -1,34 +0,0 @@
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>;
}

View File

@@ -1,67 +0,0 @@
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;

View File

@@ -1,15 +0,0 @@
{
"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"
}
}

View File

@@ -1,70 +0,0 @@
#[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;

View File

@@ -1,67 +0,0 @@
/// 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)
}

12397
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2026.2.0",
"version": "2025.12.0",
"keywords": [
"bitwarden",
"password",
@@ -26,16 +26,15 @@
"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",
"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",
"rebuild": "electron-rebuild",
"reset": "rimraf --glob ./node_modules/keytar/* && npm install",
"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 rebuild && npm run build",
"build:dist": "npm run reset && 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",
@@ -74,15 +73,17 @@
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-eslint/eslint-plugin-template": "21.1.0",
"@angular-eslint/template-parser": "21.1.0",
"@angular/build": "21.1.2",
"@angular/compiler-cli": "21.1.1",
"@angular-devkit/build-angular": "20.3.3",
"@angular-eslint/eslint-plugin-template": "20.7.0",
"@angular-eslint/template-parser": "20.7.0",
"@angular/compiler-cli": "20.3.15",
"@electron/notarize": "2.5.0",
"@electron/rebuild": "4.0.1",
"@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.43.1",
"@ngtools/webpack": "21.1.2",
"@ngtools/webpack": "20.3.3",
"@types/inquirer": "8.2.10",
"@types/jest": "30.0.0",
"@types/jest": "29.5.14",
"@types/lowdb": "1.0.15",
"@types/node": "22.19.2",
"@types/node-fetch": "2.6.12",
@@ -90,11 +91,10 @@
"@types/proper-lockfile": "4.1.4",
"@types/semver": "7.7.1",
"@types/tldjs": "2.3.4",
"@typescript-eslint/eslint-plugin": "8.54.0",
"@typescript-eslint/parser": "8.54.0",
"@typescript-eslint/eslint-plugin": "8.50.0",
"@typescript-eslint/parser": "8.50.0",
"@yao-pkg/pkg": "5.16.1",
"babel-loader": "10.0.0",
"jest-environment-jsdom": "30.2.0",
"clean-webpack-plugin": "4.0.0",
"concurrently": "9.2.0",
"copy-webpack-plugin": "13.0.0",
"cross-env": "7.0.3",
@@ -105,27 +105,28 @@
"electron-log": "5.4.1",
"electron-reload": "2.0.0-alpha.1",
"electron-store": "8.2.0",
"electron-updater": "6.7.3",
"electron-updater": "6.6.2",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.5",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-rxjs-angular-x": "0.1.0",
"eslint-plugin-rxjs-x": "0.9.1",
"eslint-plugin-rxjs-x": "0.8.3",
"form-data": "4.0.4",
"glob": "13.0.6",
"glob": "13.0.0",
"html-loader": "5.1.0",
"html-webpack-plugin": "5.6.3",
"husky": "9.1.7",
"jest": "30.2.0",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"jest-mock-extended": "4.0.0",
"jest-preset-angular": "16.0.0",
"jest-preset-angular": "14.6.0",
"lint-staged": "16.2.6",
"mini-css-extract-plugin": "2.10.0",
"mini-css-extract-plugin": "2.9.2",
"minimatch": "5.1.2",
"node-forge": "1.3.2",
"node-loader": "2.1.0",
"prettier": "3.8.1",
"prettier": "3.7.4",
"rimraf": "6.1.0",
"rxjs": "7.8.2",
"sass": "1.97.1",
@@ -133,25 +134,25 @@
"ts-jest": "29.4.1",
"ts-loader": "9.5.2",
"tsconfig-paths-webpack-plugin": "4.2.0",
"type-fest": "5.4.2",
"type-fest": "5.3.0",
"typescript": "5.9.3",
"webpack": "5.105.1",
"webpack": "5.104.1",
"webpack-cli": "6.0.1",
"webpack-merge": "6.0.1",
"webpack-node-externals": "3.0.0",
"zone.js": "0.16.0"
"zone.js": "0.15.1"
},
"dependencies": {
"@angular/animations": "21.1.1",
"@angular/cdk": "21.1.1",
"@angular/cli": "21.1.2",
"@angular/common": "21.1.1",
"@angular/compiler": "21.1.1",
"@angular/core": "21.1.1",
"@angular/forms": "21.1.1",
"@angular/platform-browser": "21.1.1",
"@angular/platform-browser-dynamic": "21.1.1",
"@angular/router": "21.1.1",
"@angular/animations": "20.3.15",
"@angular/cdk": "20.2.14",
"@angular/cli": "20.3.3",
"@angular/common": "20.3.15",
"@angular/compiler": "20.3.15",
"@angular/core": "20.3.15",
"@angular/forms": "20.3.15",
"@angular/platform-browser": "20.3.15",
"@angular/platform-browser-dynamic": "20.3.15",
"@angular/router": "20.3.15",
"@microsoft/microsoft-graph-client": "3.0.7",
"big-integer": "1.6.52",
"bootstrap": "5.3.7",
@@ -162,20 +163,20 @@
"googleapis": "149.0.0",
"https-proxy-agent": "7.0.6",
"inquirer": "8.2.6",
"dc-native": "file:./native",
"ldapts": "8.1.3",
"keytar": "7.9.0",
"ldapts": "8.0.1",
"lowdb": "1.0.0",
"ngx-toastr": "20.0.4",
"ngx-toastr": "19.1.0",
"node-fetch": "2.7.0",
"parse5": "8.0.0",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.2",
"tldjs": "2.3.1",
"uuid": "11.1.0",
"zone.js": "0.16.0"
"zone.js": "0.15.1"
},
"engines": {
"node": "~22",
"node": "~20",
"npm": "~10"
},
"lint-staged": {

View File

@@ -1,4 +1,4 @@
import { enableProdMode, provideZoneChangeDetection } from "@angular/core";
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { isDev } from "@/jslib/electron/src/utils";
@@ -11,7 +11,4 @@ if (!isDev()) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, {
applicationProviders: [provideZoneChangeDetection()],
preserveWhitespaces: true,
});
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -3,25 +3,17 @@
<div class="card-body">
<p>
{{ "lastGroupSync" | i18n }}:
@if (!lastGroupSync) {
<span>-</span>
}
<span *ngIf="!lastGroupSync">-</span>
{{ lastGroupSync | date: "medium" }}
<br />
{{ "lastUserSync" | i18n }}:
@if (!lastUserSync) {
<span>-</span>
}
<span *ngIf="!lastUserSync">-</span>
{{ lastUserSync | date: "medium" }}
</p>
<p>
{{ "syncStatus" | i18n }}:
@if (syncRunning) {
<strong class="text-success">{{ "running" | i18n }}</strong>
}
@if (!syncRunning) {
<strong class="text-danger">{{ "stopped" | i18n }}</strong>
}
<strong *ngIf="syncRunning" class="text-success">{{ "running" | i18n }}</strong>
<strong *ngIf="!syncRunning" class="text-danger">{{ "stopped" | i18n }}</strong>
</p>
<form #startForm [appApiAction]="startPromise" class="d-inline">
<button
@@ -68,85 +60,57 @@
/>
<label class="form-check-label" for="simSinceLast">{{ "testLastSync" | i18n }}</label>
</div>
@if (!simForm.loading && (simUsers || simGroups)) {
<ng-container *ngIf="!simForm.loading && (simUsers || simGroups)">
<hr />
<div class="row">
<div class="col-lg">
<h4>{{ "users" | i18n }}</h4>
@if (simEnabledUsers && simEnabledUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simEnabledUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simEnabledUsers || !simEnabledUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
<ul class="bwi-ul testing-list" *ngIf="simEnabledUsers && simEnabledUsers.length">
<li *ngFor="let u of simEnabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simEnabledUsers || !simEnabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "disabledUsers" | i18n }}</h4>
@if (simDisabledUsers && simDisabledUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simDisabledUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simDisabledUsers || !simDisabledUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
<ul class="bwi-ul testing-list" *ngIf="simDisabledUsers && simDisabledUsers.length">
<li *ngFor="let u of simDisabledUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDisabledUsers || !simDisabledUsers.length">
{{ "noUsers" | i18n }}
</p>
<h4>{{ "deletedUsers" | i18n }}</h4>
@if (simDeletedUsers && simDeletedUsers.length) {
<ul class="bwi-ul testing-list">
@for (u of simDeletedUsers; track u) {
<li title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simDeletedUsers || !simDeletedUsers.length) {
<p>
{{ "noUsers" | i18n }}
</p>
}
<ul class="bwi-ul testing-list" *ngIf="simDeletedUsers && simDeletedUsers.length">
<li *ngFor="let u of simDeletedUsers" title="{{ u.referenceId }}">
<i class="bwi bwi-li bwi-user"></i>
{{ u.displayName }}
</li>
</ul>
<p *ngIf="!simDeletedUsers || !simDeletedUsers.length">
{{ "noUsers" | i18n }}
</p>
</div>
<div class="col-lg">
<h4>{{ "groups" | i18n }}</h4>
@if (simGroups && simGroups.length) {
<ul class="bwi-ul testing-list">
@for (g of simGroups; track g) {
<li title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
@if (g.users && g.users.length) {
<ul class="small">
@for (u of g.users; track u) {
<li title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
}
</ul>
}
<ul class="bwi-ul testing-list" *ngIf="simGroups && simGroups.length">
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
}
</ul>
}
@if (!simGroups || !simGroups.length) {
<p>{{ "noGroups" | i18n }}</p>
}
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>
</div>
</div>
}
</ng-container>
</div>
</div>

View File

@@ -6,11 +6,9 @@
<div class="mb-3">
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
@for (o of directoryOptions; track o) {
<option [ngValue]="o.value">
{{ o.name }}
</option>
}
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div [hidden]="directory != directoryType.Ldap">
@@ -53,22 +51,20 @@
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
</div>
</div>
@if (!ldap.ad) {
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="pagedSearch"
[(ngModel)]="ldap.pagedSearch"
name="PagedSearch"
/>
<label class="form-check-label" for="pagedSearch">{{
"ldapPagedResults" | i18n
}}</label>
</div>
<div class="mb-3" *ngIf="!ldap.ad">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="pagedSearch"
[(ngModel)]="ldap.pagedSearch"
name="PagedSearch"
/>
<label class="form-check-label" for="pagedSearch">{{
"ldapPagedResults" | i18n
}}</label>
</div>
}
</div>
<div class="mb-3">
<div class="form-check">
<input
@@ -83,122 +79,116 @@
}}</label>
</div>
</div>
@if (ldap.ssl) {
<div class="ms-4">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div>
<div class="ms-4" *ngIf="ldap.ssl">
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div>
@if (ldap.startTls) {
<div class="ms-4">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
<input
type="text"
class="form-control"
id="tlsCaPath"
name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath"
/>
</div>
</div>
}
@if (!ldap.startTls) {
<div class="ms-4">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
<input
type="text"
class="form-control"
id="sslCertPath"
name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
<input
type="text"
class="form-control"
id="sslKeyPath"
name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
<input
type="text"
class="form-control"
id="sslCaPath"
name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"
/>
</div>
</div>
}
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized"
name="CertDoNoVerify"
/>
<label class="form-check-label" for="certDoNotVerify">{{
"ldapCertDoNotVerify" | i18n
}}</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div>
</div>
}
<div class="ms-4" *ngIf="ldap.startTls">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
<input
type="text"
class="form-control"
id="tlsCaPath"
name="TLSCaPath"
[(ngModel)]="ldap.tlsCaPath"
/>
</div>
</div>
<div class="ms-4" *ngIf="!ldap.startTls">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
<input
type="text"
class="form-control"
id="sslCertPath"
name="SSLCertPath"
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
<input
type="text"
class="form-control"
id="sslKeyPath"
name="SSLKeyPath"
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
<input
type="text"
class="form-control"
id="sslCaPath"
name="SSLCaPath"
[(ngModel)]="ldap.sslCaPath"
/>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="certDoNotVerify"
[(ngModel)]="ldap.sslAllowUnauthorized"
name="CertDoNoVerify"
/>
<label class="form-check-label" for="certDoNotVerify">{{
"ldapCertDoNotVerify" | i18n
}}</label>
</div>
</div>
</div>
<div class="mb-3" [hidden]="true">
<div class="form-check">
<input
@@ -221,12 +211,10 @@
name="Username"
[(ngModel)]="ldap.username"
/>
@if (ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} company\admin</div>
}
@if (!ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} cn=admin,dc=company,dc=com</div>
}
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
<div class="form-text" *ngIf="!ldap.ad">
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ "password" | i18n }}</label>
@@ -616,24 +604,18 @@
name="UserFilter"
[(ngModel)]="sync.userFilter"
></textarea>
@if (directory === directoryType.Ldap) {
<div class="form-text">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
}
@if (directory === directoryType.EntraID) {
<div class="form-text">{{ "ex" | i18n }} exclude:joe&#64;company.com</div>
}
@if (directory === directoryType.Okta) {
<div class="form-text">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
}
@if (directory === directoryType.GSuite) {
<div class="form-text">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div>
}
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
<div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} exclude:joe&#64;company.com
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgUnitPath=/Engineering
</div>
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
@@ -699,20 +681,18 @@
name="GroupFilter"
[(ngModel)]="sync.groupFilter"
></textarea>
@if (directory === directoryType.Ldap) {
<div class="form-text">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
}
@if (directory === directoryType.EntraID) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
}
@if (directory === directoryType.Okta) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</div>
}
@if (directory === directoryType.GSuite) {
<div class="form-text">{{ "ex" | i18n }} include:Sales,IT</div>
}
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
<div class="form-text" *ngIf="directory === directoryType.EntraID">
{{ "ex" | i18n }} include:Sales,IT
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} include:Sales,IT
</div>
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
@@ -723,12 +703,8 @@
name="GroupPath"
[(ngModel)]="sync.groupPath"
/>
@if (!ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} CN=Groups</div>
}
@if (ldap.ad) {
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
}
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="mb-3">

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 NativeSecureStorageService(applicationName);
: new KeytarSecureStorageService(applicationName);
this.stateMigrationService = new StateMigrationService(
this.storageService,

View File

@@ -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("nativeSecureStorage", async (event: any, message: any) => {
ipcMain.on("keytar", 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 passwords.getPassword(serviceName, message.key);
val = await getPassword(serviceName, message.key);
} else if (message.action === "hasPassword") {
const result = await passwords.getPassword(serviceName, message.key);
const result = await getPassword(serviceName, message.key);
val = result != null;
} else if (message.action === "setPassword" && message.value) {
await passwords.setPassword(serviceName, message.key, message.value);
await setPassword(serviceName, message.key, message.value);
} else if (message.action === "deletePassword") {
await passwords.deletePassword(serviceName, message.key);
await deletePassword(serviceName, message.key);
}
}
event.returnValue = val;

View File

@@ -28,4 +28,4 @@ $danger: map_get($theme-colors, "danger");
$secondary: map_get($theme-colors, "secondary");
$secondary-alt: map_get($theme-colors, "secondary-alt");
@import "bootstrap/scss/bootstrap.scss";
@import "~bootstrap/scss/bootstrap.scss";

View File

@@ -1,4 +1,4 @@
@import "bootstrap/scss/_variables.scss";
@import "~bootstrap/scss/_variables.scss";
html.os_windows {
body {

View File

@@ -1,4 +1,4 @@
@import "bootstrap/scss/_variables.scss";
@import "~bootstrap/scss/_variables.scss";
body {
padding: 10px 0 20px 0;

View File

@@ -1,6 +1,6 @@
@import "ngx-toastr/toastr";
@import "~ngx-toastr/toastr";
@import "bootstrap/scss/_variables.scss";
@import "~bootstrap/scss/_variables.scss";
.toast-container {
.toast-close-button {

View File

@@ -1,8 +1,7 @@
import { mock } from "jest-mock-extended";
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import {
@@ -12,6 +11,7 @@ import {
} from "@/jslib/common/src/models/domain/account";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service";
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
import { AuthService } from "./auth.service";
@@ -35,22 +35,22 @@ export function identityTokenResponseFactory() {
}
describe("AuthService", () => {
let apiService: jest.Mocked<ApiService>;
let appIdService: jest.Mocked<AppIdService>;
let platformUtilsService: jest.Mocked<PlatformUtilsService>;
let messagingService: jest.Mocked<MessagingService>;
let stateService: jest.Mocked<StateService>;
let apiService: SubstituteOf<ApiService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let stateService: SubstituteOf<StateService>;
let authService: AuthService;
beforeEach(async () => {
apiService = mock<ApiService>();
appIdService = mock<AppIdService>();
platformUtilsService = mock<PlatformUtilsService>();
stateService = mock<StateService>();
messagingService = mock<MessagingService>();
apiService = Substitute.for();
appIdService = Substitute.for();
platformUtilsService = Substitute.for();
stateService = Substitute.for();
messagingService = Substitute.for();
appIdService.getAppId.mockResolvedValue(deviceId);
appIdService.getAppId().resolves(deviceId);
authService = new AuthService(
apiService,
@@ -62,12 +62,11 @@ describe("AuthService", () => {
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await authService.logIn({ clientId, clientSecret });
expect(stateService.addAccount).toHaveBeenCalledTimes(1);
expect(stateService.addAccount).toHaveBeenCalledWith(
stateService.received(1).addAccount(
new Account({
profile: {
...new AccountProfile(),

View File

@@ -68,12 +68,10 @@ export class LdapDirectoryService implements IDirectoryService {
}
groups = await this.getGroups(groupForce);
}
} catch (e) {
} finally {
await this.client.unbind();
throw e;
}
await this.client.unbind();
return [groups, users];
}
@@ -455,9 +453,8 @@ export class LdapDirectoryService implements IDirectoryService {
try {
await this.client.bind(user, pass);
} catch (error) {
} catch {
await this.client.unbind();
throw error;
}
}

View File

@@ -0,0 +1,31 @@
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

@@ -1,28 +0,0 @@
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);
}
}

View File

@@ -1,5 +1,3 @@
import { passwords } from "dc-native";
import { StateVersion } from "@/jslib/common/src/enums/stateVersion";
import { StateMigrationService as BaseStateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
@@ -63,13 +61,6 @@ 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;
}
@@ -177,50 +168,6 @@ 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

@@ -1,7 +1,7 @@
import { webcrypto } from "crypto";
import { TextEncoder, TextDecoder } from "util";
Object.assign(globalThis, { TextEncoder, TextDecoder });
import "jest-preset-angular/setup-jest";
Object.defineProperty(window, "CSS", { value: null });
Object.defineProperty(window, "getComputedStyle", {
value: () => {

View File

@@ -5,7 +5,7 @@
},
"compilerOptions": {
"pretty": true,
"moduleResolution": "bundler",
"moduleResolution": "node",
"noImplicitAny": true,
"target": "ES2016",
"module": "ES2020",

View File

@@ -1,5 +1,6 @@
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");
@@ -23,6 +24,7 @@ const moduleRules = [
];
const plugins = [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [{ from: "./src/locales", to: "locales" }],
}),
@@ -62,7 +64,6 @@ const config = {
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build-cli"),
clean: true,
},
module: { rules: moduleRules },
plugins: plugins,

View File

@@ -1,6 +1,7 @@
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");
@@ -22,7 +23,6 @@ const common = {
output: {
filename: "[name].js",
path: path.resolve(__dirname, "build"),
clean: true,
},
};
@@ -48,6 +48,7 @@ const main = {
],
},
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
"./package.json",
@@ -58,7 +59,7 @@ const main = {
],
externals: {
"electron-reload": "commonjs2 electron-reload",
"dc-native": "commonjs2 dc-native",
keytar: "commonjs2 keytar",
},
};

View File

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