mirror of
https://github.com/bitwarden/browser
synced 2026-02-09 21:20:27 +00:00
Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows
This commit is contained in:
11
.github/DISCUSSION_TEMPLATE/password-manager.yml
vendored
11
.github/DISCUSSION_TEMPLATE/password-manager.yml
vendored
@@ -1,8 +1,19 @@
|
||||
labels: ["discussions-new"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Select Topic Area
|
||||
description: "What would you like to discuss? :warning: For feature requests and product feedback, please visit https://community.bitwarden.com/"
|
||||
options:
|
||||
- "✅ Code Contribution Proposal"
|
||||
- "🚫 Product Feedback"
|
||||
- "🚫 Feature Request"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Code Contribution Proposal
|
||||
|
||||
11
.github/DISCUSSION_TEMPLATE/secrets-manager.yml
vendored
11
.github/DISCUSSION_TEMPLATE/secrets-manager.yml
vendored
@@ -1,8 +1,19 @@
|
||||
labels: ["discussions-new"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you would like to contribute code to the Bitwarden codebase for consideration, please review [https://contributing.bitwarden.com/](https://contributing.bitwarden.com/) before posting. To keep discussion on topic, posts that do not include a proposal for a code contribution you wish to develop will be removed. For feature requests and community discussion, please visit https://community.bitwarden.com/
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Select Topic Area
|
||||
description: "What would you like to discuss? :warning: For feature requests and product feedback, please visit https://community.bitwarden.com/"
|
||||
options:
|
||||
- "✅ Code Contribution Proposal"
|
||||
- "🚫 Product Feedback"
|
||||
- "🚫 Feature Request"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Code Contribution Proposal
|
||||
|
||||
28
.github/renovate.json5
vendored
28
.github/renovate.json5
vendored
@@ -4,10 +4,10 @@
|
||||
enabledManagers: ["cargo", "github-actions", "npm"],
|
||||
packageRules: [
|
||||
{
|
||||
// Group all build/test/lint workflows for GitHub Actions together for Platform
|
||||
// Since they are code owners we don't need to assign a review team in Renovate
|
||||
// Any changes here should also be reflected in CODEOWNERS
|
||||
groupName: "github-action minor",
|
||||
// Group all build/test/lint workflows for GitHub Actions together for Platform.
|
||||
// Since they are code owners we don't need to assign a review team in Renovate.
|
||||
// Any changes here should also be reflected in CODEOWNERS.
|
||||
groupName: "github-action",
|
||||
matchManagers: ["github-actions"],
|
||||
matchFileNames: [
|
||||
"./github/workflows/automatic-issue-responses.yml",
|
||||
@@ -30,10 +30,10 @@
|
||||
commitMessagePrefix: "[deps] Platform:",
|
||||
},
|
||||
{
|
||||
// Group all release-related workflows for GitHub Actions together for BRE
|
||||
// Since they are code owners we don't need to assign a review team in Renovate
|
||||
// Any changes here should also be reflected in CODEOWNERS
|
||||
groupName: "github-action minor",
|
||||
// Group all release-related workflows for GitHub Actions together for BRE.
|
||||
// Since they are code owners we don't need to assign a review team in Renovate.
|
||||
// Any changes here should also be reflected in CODEOWNERS.
|
||||
groupName: "github-action",
|
||||
matchManagers: ["github-actions"],
|
||||
matchFileNames: [
|
||||
"./github/workflows/brew-bump-desktop.yml",
|
||||
@@ -51,7 +51,7 @@
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
},
|
||||
{
|
||||
// Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular
|
||||
// Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular.
|
||||
matchPackageNames: ["typescript", "zone.js"],
|
||||
matchUpdateTypes: ["major", "minor"],
|
||||
description: "Determined by Angular",
|
||||
@@ -72,27 +72,27 @@
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
// Renovate should manage patch updates for TypeScript and Zone.js, despite ignoring major and minor
|
||||
// Renovate should manage patch updates for TypeScript and Zone.js, despite ignoring major and minor.
|
||||
matchPackageNames: ["typescript", "zone.js"],
|
||||
matchUpdateTypes: "patch",
|
||||
},
|
||||
{
|
||||
// We want to update all the Jest-related packages together, to reduce PR noise
|
||||
// We want to update all the Jest-related packages together, to reduce PR noise.
|
||||
groupName: "jest",
|
||||
matchPackageNames: ["@types/jest", "jest", "ts-jest", "jest-preset-angular"],
|
||||
},
|
||||
{
|
||||
// We need to group all napi-related packages together to avoid build errors caused by version incompatibilities
|
||||
// We need to group all napi-related packages together to avoid build errors caused by version incompatibilities.
|
||||
groupName: "napi",
|
||||
matchPackageNames: ["napi", "napi-build", "napi-derive"],
|
||||
},
|
||||
{
|
||||
// We need to group all macOS/iOS binding-related packages together to avoid build errors caused by version incompatibilities
|
||||
// We need to group all macOS/iOS binding-related packages together to avoid build errors caused by version incompatibilities.
|
||||
groupName: "macOS/iOS bindings",
|
||||
matchPackageNames: ["core-foundation", "security-framework", "security-framework-sys"],
|
||||
},
|
||||
{
|
||||
// We need to group all zbus-related packages together to avoid build errors caused by version incompatibilities
|
||||
// We need to group all zbus-related packages together to avoid build errors caused by version incompatibilities.
|
||||
groupName: "zbus",
|
||||
matchPackageNames: ["zbus", "zbus_polkit"],
|
||||
},
|
||||
|
||||
112
.github/workflows/auto-reply-discussions.yml
vendored
Normal file
112
.github/workflows/auto-reply-discussions.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: Auto-reply to Discussions
|
||||
|
||||
on:
|
||||
discussion:
|
||||
types: created
|
||||
|
||||
jobs:
|
||||
reply:
|
||||
name: Auto-reply
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Get discussion label and template name
|
||||
id: discussion-label
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const discussion = context.payload.discussion;
|
||||
const template_name = discussion.category.slug;
|
||||
const label = discussion.labels?.[0]?.name ?? '';
|
||||
console.log('Discussion label:', label);
|
||||
console.log('Discussion category slug:', template_name);
|
||||
|
||||
core.setOutput('label', label);
|
||||
core.setOutput('template_name', template_name);
|
||||
|
||||
- name: Get selected topic
|
||||
id: get_selected_topic
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
try {
|
||||
const body = context.payload.discussion.body;
|
||||
const match = body.match(/### Select Topic Area\n\n(.*?)\n\n/);
|
||||
console.log('Match:', match);
|
||||
console.log('Match1:', match[1]);
|
||||
return match ? match[1].trim() : "";
|
||||
} catch (error) {
|
||||
console.error('Error getting selected topic:', error);
|
||||
return "";
|
||||
}
|
||||
|
||||
- name: Reply or close Discussion
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
env:
|
||||
TEMPLATE_NAME: ${{ steps.discussion-label.outputs.template_name }}
|
||||
TOPIC: ${{ steps.get_selected_topic.outputs.result }}
|
||||
with:
|
||||
script: |
|
||||
async function addComment(discussionId, body) {
|
||||
await github.graphql(`
|
||||
mutation AddDiscussionComment($discussionId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
discussionId,
|
||||
body
|
||||
});
|
||||
}
|
||||
|
||||
async function closeDiscussion(discussionId) {
|
||||
await github.graphql(`
|
||||
mutation {
|
||||
closeDiscussion(input: {discussionId: "${discussionId}"}) {
|
||||
discussion {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
console.log('♻️ Closing discussion');
|
||||
}
|
||||
|
||||
const discussion = context.payload.discussion;
|
||||
const isFeatureRequest = process.env.TEMPLATE_NAME === 'feature-request';
|
||||
const isTopicEmpty = !process.env.TOPIC || process.env.TOPIC.trim() === ''; // topic step may have failed
|
||||
const isCodeTopic = process.env.TOPIC.includes('Code Contribution Proposal');
|
||||
const shouldClose = isFeatureRequest || (!isTopicEmpty && !isCodeTopic);
|
||||
|
||||
console.log('Template name:', process.env.TEMPLATE_NAME);
|
||||
console.log('Topic:', process.env.TOPIC);
|
||||
console.log('isTopicEmpty:', isTopicEmpty);
|
||||
console.log('isCodeTopic:', isCodeTopic);
|
||||
console.log('shouldClose:', shouldClose);
|
||||
|
||||
if (shouldClose) {
|
||||
const closeMessage =
|
||||
"Thank you for your contribution! GitHub Discussions is specifically for [proposing code](https://contributing.bitwarden.com/) that you would like to write for the Bitwarden codebase. Since this post does not appear to include a proposal, it will be closed. If you believe this was done in error or have any questions, please feel free to reach out to us!\n\n" +
|
||||
"- :bulb: For feature requests and general discussions, please visit the [Bitwarden Community Forums](https://community.bitwarden.com/).\n" +
|
||||
"- :information_source: For questions and support, visit the [Bitwarden Help Center](https://bitwarden.com/help/).\n" +
|
||||
"- :bug: To report a potential bug, please visit the appropriate repository: [Server](https://github.com/bitwarden/server/issues) | [Clients](https://github.com/bitwarden/clients/issues) | [iOS](https://github.com/bitwarden/ios/issues) | [Android](https://github.com/bitwarden/android/issues).";
|
||||
|
||||
await addComment(discussion.node_id, closeMessage);
|
||||
await closeDiscussion(discussion.node_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const replyMessage =
|
||||
":sparkles: Thank you for your code contribution proposal! While the Bitwarden team reviews your submission, we encourage you to check out our [contribution guidelines](https://contributing.bitwarden.com/).\n\n" +
|
||||
"Please ensure that your code contribution includes a detailed description of what you would like to contribute, along with any relevant screenshots and links to existing feature requests. This information helps us gather feedback from the community and Bitwarden team members before you start writing code.\n\n" +
|
||||
"To keep discussions focused, posts that do not include a proposal for a code contribution will be removed.\n\n" +
|
||||
"- :bulb: For feature requests and general discussion, please visit the [Bitwarden Community Forums](https://community.bitwarden.com/).\n" +
|
||||
"- :information_source: For questions and support, visit the [Bitwarden Help Center](https://bitwarden.com/help/).\n" +
|
||||
"- :bug: To report a potential bug, please visit the corresponding repo. [Server](https://github.com/bitwarden/server/issues) | [Clients](https://github.com/bitwarden/clients/issues) | [iOS](https://github.com/bitwarden/ios/issues) | [Android](https://github.com/bitwarden/android/issues)\n\n" +
|
||||
"Thank you for contributing to Bitwarden!";
|
||||
|
||||
await addComment(discussion.node_id, replyMessage);
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
crowdin_project_id: "308189"
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
|
||||
6
.github/workflows/repository-management.yml
vendored
6
.github/workflows/repository-management.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@@ -452,7 +452,7 @@ jobs:
|
||||
- setup
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
|
||||
2
.github/workflows/version-auto-bump.yml
vendored
2
.github/workflows/version-auto-bump.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
|
||||
@@ -2297,6 +2297,9 @@
|
||||
"privacyPolicy": {
|
||||
"message": "Privacy Policy"
|
||||
},
|
||||
"yourNewPasswordCannotBeTheSameAsYourCurrentPassword": {
|
||||
"message": "Your new password cannot be the same as your current password."
|
||||
},
|
||||
"hintEqualsPassword": {
|
||||
"message": "Your password hint cannot be the same as your password."
|
||||
},
|
||||
|
||||
@@ -111,6 +111,7 @@ import {
|
||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { StateFactory } from "@bitwarden/common/platform/factories/state-factory";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
|
||||
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
|
||||
@@ -259,6 +260,7 @@ import { BackgroundBrowserBiometricsService } from "../key-management/biometrics
|
||||
import VaultTimeoutService from "../key-management/vault-timeout/vault-timeout.service";
|
||||
import { BrowserApi } from "../platform/browser/browser-api";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { IpcBackgroundService } from "../platform/ipc/ipc-background.service";
|
||||
import { UpdateBadge } from "../platform/listeners/update-badge";
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender";
|
||||
@@ -403,6 +405,8 @@ export default class MainBackground {
|
||||
inlineMenuFieldQualificationService: InlineMenuFieldQualificationService;
|
||||
taskService: TaskService;
|
||||
|
||||
ipcService: IpcService;
|
||||
|
||||
onUpdatedRan: boolean;
|
||||
onReplacedRan: boolean;
|
||||
loginToAutoFill: CipherView = null;
|
||||
@@ -1309,6 +1313,8 @@ export default class MainBackground {
|
||||
);
|
||||
|
||||
this.inlineMenuFieldQualificationService = new InlineMenuFieldQualificationService();
|
||||
|
||||
this.ipcService = new IpcBackgroundService(this.logService);
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
@@ -1382,6 +1388,7 @@ export default class MainBackground {
|
||||
}
|
||||
|
||||
await this.initOverlayAndTabsBackground();
|
||||
await this.ipcService.init();
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(async () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc";
|
||||
import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue";
|
||||
import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { BrowserApi } from "../browser/browser-api";
|
||||
|
||||
export class BackgroundCommunicationBackend implements CommunicationBackend {
|
||||
private queue = new MessageQueue<IncomingMessage>();
|
||||
|
||||
constructor() {
|
||||
BrowserApi.messageListener("platform.ipc", (message, sender) => {
|
||||
if (!isIpcMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender.tab?.id === undefined || sender.tab.id === chrome.tabs.TAB_ID_NONE) {
|
||||
// Ignore messages from non-tab sources
|
||||
return;
|
||||
}
|
||||
|
||||
void this.queue.enqueue({ ...message.message, source: { Web: { id: sender.tab.id } } });
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage): Promise<void> {
|
||||
if (typeof message.destination === "object" && "Web" in message.destination) {
|
||||
await BrowserApi.tabSendMessage(
|
||||
{ id: message.destination.Web.id } as chrome.tabs.Tab,
|
||||
{ type: "bitwarden-ipc-message", message } satisfies IpcMessage,
|
||||
{ frameId: 0 },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Destination not supported.");
|
||||
}
|
||||
|
||||
async receive(): Promise<IncomingMessage> {
|
||||
return this.queue.dequeue();
|
||||
}
|
||||
}
|
||||
51
apps/browser/src/platform/ipc/content/ipc-content-script.ts
Normal file
51
apps/browser/src/platform/ipc/content/ipc-content-script.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// TODO: This content script should be dynamically reloaded when the extension is updated,
|
||||
// to avoid "Extension context invalidated." errors.
|
||||
|
||||
import { isIpcMessage } from "@bitwarden/common/platform/ipc/ipc-message";
|
||||
|
||||
// Web -> Background
|
||||
export function sendExtensionMessage(message: unknown) {
|
||||
if (
|
||||
typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.sendMessage !== "undefined"
|
||||
) {
|
||||
void browser.runtime.sendMessage(message);
|
||||
return;
|
||||
}
|
||||
|
||||
void chrome.runtime.sendMessage(message);
|
||||
}
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isIpcMessage(event.data)) {
|
||||
sendExtensionMessage(event.data);
|
||||
}
|
||||
});
|
||||
|
||||
// Background -> Web
|
||||
function setupMessageListener() {
|
||||
function listener(message: unknown) {
|
||||
if (isIpcMessage(message)) {
|
||||
void window.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
typeof browser !== "undefined" &&
|
||||
typeof browser.runtime !== "undefined" &&
|
||||
typeof browser.runtime.onMessage !== "undefined"
|
||||
) {
|
||||
browser.runtime.onMessage.addListener(listener);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax -- This doesn't run in the popup but in the content script
|
||||
chrome.runtime.onMessage.addListener(listener);
|
||||
}
|
||||
|
||||
setupMessageListener();
|
||||
26
apps/browser/src/platform/ipc/ipc-background.service.ts
Normal file
26
apps/browser/src/platform/ipc/ipc-background.service.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { IpcClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { BackgroundCommunicationBackend } from "./background-communication-backend";
|
||||
|
||||
export class IpcBackgroundService extends IpcService {
|
||||
private communicationProvider?: BackgroundCommunicationBackend;
|
||||
|
||||
constructor(private logService: LogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
override async init() {
|
||||
try {
|
||||
// This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load.
|
||||
await SdkLoadService.Ready;
|
||||
this.communicationProvider = new BackgroundCommunicationBackend();
|
||||
|
||||
await super.initWithClient(new IpcClient(this.communicationProvider));
|
||||
} catch (e) {
|
||||
this.logService.error("[IPC] Initialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +205,7 @@ const mainConfig = {
|
||||
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
|
||||
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",
|
||||
"content/fido2-page-script": "./src/autofill/fido2/content/fido2-page-script.ts",
|
||||
"content/ipc-content-script": "./src/platform/ipc/content/ipc-content-script.ts",
|
||||
"notification/bar": "./src/autofill/notification/bar.ts",
|
||||
"overlay/menu-button":
|
||||
"./src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts",
|
||||
|
||||
@@ -2072,6 +2072,9 @@
|
||||
"personalOwnershipSubmitError": {
|
||||
"message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections."
|
||||
},
|
||||
"yourNewPasswordCannotBeTheSameAsYourCurrentPassword": {
|
||||
"message": "Your new password cannot be the same as your current password."
|
||||
},
|
||||
"hintEqualsPassword": {
|
||||
"message": "Your password hint cannot be the same as your password."
|
||||
},
|
||||
|
||||
@@ -187,11 +187,11 @@ describe("WebRegistrationFinishService", () => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -239,7 +239,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -277,7 +277,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -320,7 +320,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -365,7 +365,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
@@ -412,7 +412,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
|
||||
@@ -59,8 +59,8 @@ export class FreeFamiliesPolicyService {
|
||||
return false;
|
||||
}
|
||||
const { belongToOneEnterpriseOrgs, isFreeFamilyPolicyEnabled } = orgStatus;
|
||||
const canManageSponsorships = organizations.filter((org) => org.canManageSponsorships);
|
||||
return canManageSponsorships && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled);
|
||||
const hasSponsorshipOrgs = organizations.some((org) => org.canManageSponsorships);
|
||||
return hasSponsorshipOrgs && !(belongToOneEnterpriseOrgs && isFreeFamilyPolicyEnabled);
|
||||
}
|
||||
|
||||
checkEnterpriseOrganizationsAndFetchPolicy(): Observable<EnterpriseOrgStatus> {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<div *ngIf="!useTrialStepper">
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</div>
|
||||
<div *ngIf="useTrialStepper">
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
|
||||
@@ -6,7 +6,11 @@ import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordInputResult, RegistrationFinishService } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InputPasswordFlow,
|
||||
PasswordInputResult,
|
||||
RegistrationFinishService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
@@ -47,6 +51,8 @@ export type InitiationPath =
|
||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
InputPasswordFlow = InputPasswordFlow;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
product: ProductType;
|
||||
/** The tier of product being subscribed to */
|
||||
@@ -363,7 +369,7 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.logIn(passwordInputResult.password, captchaToken);
|
||||
await this.logIn(passwordInputResult.newPassword, captchaToken);
|
||||
|
||||
this.submitting = false;
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sd
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
// eslint-disable-next-line no-restricted-imports -- Needed for DI
|
||||
import {
|
||||
UnsupportedWebPushConnectionService,
|
||||
@@ -122,9 +123,11 @@ import { WebSsoComponentService } from "../auth/core/services/login/web-sso-comp
|
||||
import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service";
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.service";
|
||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||
import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service";
|
||||
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
import { WebIpcService } from "../platform/ipc/web-ipc.service";
|
||||
import { WebEnvironmentService } from "../platform/web-environment.service";
|
||||
import { WebMigrationRunner } from "../platform/web-migration-runner";
|
||||
import { WebSdkLoadService } from "../platform/web-sdk-load.service";
|
||||
@@ -135,7 +138,6 @@ import { InitService } from "./init.service";
|
||||
import { ENV_URLS } from "./injection-tokens";
|
||||
import { ModalService } from "./modal.service";
|
||||
import { RouterService } from "./router.service";
|
||||
import { WebFileDownloadService } from "./web-file-download.service";
|
||||
import { WebPlatformUtilsService } from "./web-platform-utils.service";
|
||||
|
||||
/**
|
||||
@@ -368,6 +370,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebLoginDecryptionOptionsService,
|
||||
deps: [MessagingService, RouterService, AcceptOrganizationInviteService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: IpcService,
|
||||
useClass: WebIpcService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
@@ -38,6 +39,7 @@ export class InitService {
|
||||
private userAutoUnlockKeyService: UserAutoUnlockKeyService,
|
||||
private accountService: AccountService,
|
||||
private versionService: VersionService,
|
||||
private ipcService: IpcService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
private configService: ConfigService,
|
||||
private bulkEncryptService: BulkEncryptService,
|
||||
@@ -72,6 +74,7 @@ export class InitService {
|
||||
htmlEl.classList.add("locale_" + this.i18nService.translationLocale);
|
||||
this.themingService.applyThemeChangesTo(this.document);
|
||||
this.versionService.applyVersionToWindow();
|
||||
void this.ipcService.init();
|
||||
|
||||
const containerService = new ContainerService(this.keyService, this.encryptService);
|
||||
containerService.attachToGlobal(this.win);
|
||||
|
||||
41
apps/web/src/app/platform/ipc/web-communication-provider.ts
Normal file
41
apps/web/src/app/platform/ipc/web-communication-provider.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { IpcMessage, isIpcMessage } from "@bitwarden/common/platform/ipc";
|
||||
import { MessageQueue } from "@bitwarden/common/platform/ipc/message-queue";
|
||||
import { CommunicationBackend, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class WebCommunicationProvider implements CommunicationBackend {
|
||||
private queue = new MessageQueue<IncomingMessage>();
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("message", async (event: MessageEvent) => {
|
||||
if (event.origin !== window.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = event.data;
|
||||
if (!isIpcMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.queue.enqueue({ ...message.message, source: "BrowserBackground" });
|
||||
});
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage): Promise<void> {
|
||||
if (message.destination === "BrowserBackground") {
|
||||
window.postMessage(
|
||||
{ type: "bitwarden-ipc-message", message } satisfies IpcMessage,
|
||||
window.location.origin,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`Destination not supported: ${message.destination}`);
|
||||
}
|
||||
|
||||
receive(): Promise<IncomingMessage> {
|
||||
return this.queue.dequeue();
|
||||
}
|
||||
}
|
||||
25
apps/web/src/app/platform/ipc/web-ipc.service.ts
Normal file
25
apps/web/src/app/platform/ipc/web-ipc.service.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { inject } from "@angular/core";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { IpcService } from "@bitwarden/common/platform/ipc";
|
||||
import { IpcClient } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { WebCommunicationProvider } from "./web-communication-provider";
|
||||
|
||||
export class WebIpcService extends IpcService {
|
||||
private logService = inject(LogService);
|
||||
private communicationProvider?: WebCommunicationProvider;
|
||||
|
||||
override async init() {
|
||||
try {
|
||||
// This function uses classes and functions defined in the SDK, so we need to wait for the SDK to load.
|
||||
await SdkLoadService.Ready;
|
||||
this.communicationProvider = new WebCommunicationProvider();
|
||||
|
||||
await super.initWithClient(new IpcClient(this.communicationProvider));
|
||||
} catch (e) {
|
||||
this.logService.error("[IPC] Initialization failed", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5707,6 +5707,9 @@
|
||||
"webAuthnSuccess": {
|
||||
"message": "WebAuthn verified successfully! You may close this tab."
|
||||
},
|
||||
"yourNewPasswordCannotBeTheSameAsYourCurrentPassword": {
|
||||
"message": "Your new password cannot be the same as your current password."
|
||||
},
|
||||
"hintEqualsPassword": {
|
||||
"message": "Your password hint cannot be the same as your password."
|
||||
},
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<div class="container page-content" *ngIf="!loading">
|
||||
<div class="page-header">
|
||||
<div class="tw-max-w-4xl tw-mx-auto" *ngIf="!loading">
|
||||
<div class="tw-mt-5">
|
||||
<h1>{{ "setupProvider" | i18n }}</h1>
|
||||
</div>
|
||||
<p>{{ "setupProviderDesc" | i18n }}</p>
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2>
|
||||
<h2 class="tw-mt-5">{{ "generalInformation" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-flow-col tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-6">
|
||||
<bit-form-field>
|
||||
|
||||
@@ -4,14 +4,36 @@
|
||||
[policy]="masterPasswordPolicyOptions"
|
||||
></auth-password-callout>
|
||||
|
||||
<bit-form-field
|
||||
*ngIf="
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
"
|
||||
>
|
||||
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_current-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="currentPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassword" | i18n }}</bit-label>
|
||||
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_password"
|
||||
id="input-password-form_new-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="password"
|
||||
formControlName="newPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -30,7 +52,7 @@
|
||||
<tools-password-strength
|
||||
[showText]="true"
|
||||
[email]="email"
|
||||
[password]="formGroup.controls.password.value"
|
||||
[password]="formGroup.controls.newPassword.value"
|
||||
(passwordStrengthScore)="getPasswordStrengthScore($event)"
|
||||
></tools-password-strength>
|
||||
</div>
|
||||
@@ -38,10 +60,10 @@
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_confirmed-password"
|
||||
id="input-password-form_confirm-new-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmedPassword"
|
||||
formControlName="confirmNewPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -65,16 +87,40 @@
|
||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
[block]="btnBlock"
|
||||
[loading]="loading"
|
||||
<bit-form-control
|
||||
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
>
|
||||
{{ buttonText || ("setMasterPassword" | i18n) }}
|
||||
</button>
|
||||
<input type="checkbox" bitCheckbox formControlName="rotateUserKey" />
|
||||
<bit-label>
|
||||
{{ "rotateAccountEncKey" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'impactOfRotatingYourEncryptionKey' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div class="tw-flex tw-gap-2" [ngClass]="inlineButtons ? 'tw-flex-row' : 'tw-flex-col'">
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [loading]="loading">
|
||||
{{ primaryButtonTextStr || ("setMasterPassword" | i18n) }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
*ngIf="secondaryButtonText"
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[loading]="loading"
|
||||
(click)="onSecondaryButtonClick.emit()"
|
||||
>
|
||||
{{ secondaryButtonTextStr }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
Translation,
|
||||
} from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -36,6 +35,29 @@ import { PasswordCalloutComponent } from "../password-callout/password-callout.c
|
||||
|
||||
import { PasswordInputResult } from "./password-input-result";
|
||||
|
||||
/**
|
||||
* Determines which form input elements will be displayed in the UI.
|
||||
*/
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* - Input: New password
|
||||
* - Input: Confirm new password
|
||||
* - Input: Hint
|
||||
* - Checkbox: Check for breaches
|
||||
*/
|
||||
SetInitialPassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Input: Current password (as the first element in the UI)
|
||||
*/
|
||||
ChangePassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Checkbox: Rotate account encryption key (as the last element in the UI)
|
||||
*/
|
||||
ChangePasswordWithOptionalUserKeyRotation,
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-input-password",
|
||||
@@ -54,44 +76,58 @@ import { PasswordInputResult } from "./password-input-result";
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class InputPasswordComponent {
|
||||
export class InputPasswordComponent implements OnInit {
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||
|
||||
@Input({ required: true }) email: string;
|
||||
@Input() buttonText: string;
|
||||
@Input({ required: true }) inputPasswordFlow!: InputPasswordFlow;
|
||||
@Input({ required: true }) email!: string;
|
||||
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() btnBlock: boolean = true;
|
||||
|
||||
@Input() inlineButtons = false;
|
||||
@Input() primaryButtonText?: Translation;
|
||||
protected primaryButtonTextStr: string = "";
|
||||
@Input() secondaryButtonText?: Translation;
|
||||
protected secondaryButtonTextStr: string = "";
|
||||
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
private minHintLength = 0;
|
||||
protected maxHintLength = 50;
|
||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||
protected minPasswordMsg = "";
|
||||
protected passwordStrengthScore: PasswordStrengthScore;
|
||||
protected passwordStrengthScore: PasswordStrengthScore = 0;
|
||||
protected showErrorSummary = false;
|
||||
protected showPassword = false;
|
||||
|
||||
protected formGroup = this.formBuilder.group(
|
||||
protected formGroup = this.formBuilder.nonNullable.group(
|
||||
{
|
||||
password: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmedPassword: ["", Validators.required],
|
||||
newPassword: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmNewPassword: ["", Validators.required],
|
||||
hint: [
|
||||
"", // must be string (not null) because we check length in validation
|
||||
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
||||
],
|
||||
checkForBreaches: true,
|
||||
checkForBreaches: [true],
|
||||
},
|
||||
{
|
||||
validators: [
|
||||
InputsFieldMatch.compareInputs(
|
||||
"doNotMatch",
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
this.i18nService.t("yourNewPasswordCannotBeTheSameAsYourCurrentPassword"),
|
||||
),
|
||||
InputsFieldMatch.compareInputs(
|
||||
"match",
|
||||
"password",
|
||||
"confirmedPassword",
|
||||
"newPassword",
|
||||
"confirmNewPassword",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
InputsFieldMatch.compareInputs(
|
||||
"doNotMatch",
|
||||
"password",
|
||||
"newPassword",
|
||||
"hint",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
@@ -109,6 +145,41 @@ export class InputPasswordComponent {
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"currentPassword",
|
||||
this.formBuilder.control("", Validators.required),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"rotateUserKey",
|
||||
this.formBuilder.control(false),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.primaryButtonText) {
|
||||
this.primaryButtonTextStr = this.i18nService.t(
|
||||
this.primaryButtonText.key,
|
||||
...(this.primaryButtonText?.placeholders ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.secondaryButtonText) {
|
||||
this.secondaryButtonTextStr = this.i18nService.t(
|
||||
this.secondaryButtonText.key,
|
||||
...(this.secondaryButtonText?.placeholders ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
@@ -132,10 +203,10 @@ export class InputPasswordComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.formGroup.controls.password.value;
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
|
||||
const passwordEvaluatedSuccessfully = await this.evaluatePassword(
|
||||
password,
|
||||
const passwordEvaluatedSuccessfully = await this.evaluateNewPassword(
|
||||
newPassword,
|
||||
this.passwordStrengthScore,
|
||||
this.formGroup.controls.checkForBreaches.value,
|
||||
);
|
||||
@@ -152,38 +223,55 @@ export class InputPasswordComponent {
|
||||
}
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
password,
|
||||
newPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
const masterKeyHash = await this.keyService.hashMasterKey(password, masterKey);
|
||||
const serverMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
password,
|
||||
newPassword,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
this.onPasswordFormSubmit.emit({
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
localMasterKeyHash,
|
||||
kdfConfig,
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
password,
|
||||
});
|
||||
kdfConfig,
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
};
|
||||
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
passwordInputResult.currentPassword = this.formGroup.get("currentPassword")?.value;
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
|
||||
}
|
||||
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
};
|
||||
|
||||
// Returns true if the password passes all checks, false otherwise
|
||||
private async evaluatePassword(
|
||||
password: string,
|
||||
private async evaluateNewPassword(
|
||||
newPassword: string,
|
||||
passwordStrengthScore: PasswordStrengthScore,
|
||||
checkForBreaches: boolean,
|
||||
) {
|
||||
// Check if the password is breached, weak, or both
|
||||
const passwordIsBreached =
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(password));
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword));
|
||||
|
||||
const passwordWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
|
||||
@@ -224,7 +312,7 @@ export class InputPasswordComponent {
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthScore,
|
||||
password,
|
||||
newPassword,
|
||||
this.masterPasswordPolicyOptions,
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter a master password and hint. On submission it
|
||||
creates a master key, master key hash, and emits those values to the parent (along with the hint and
|
||||
default kdfConfig).
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On
|
||||
submission it creates a master key, master key hash, and emits those values to the parent (along
|
||||
with the other values found in `PasswordInputResult`).
|
||||
|
||||
The component is intended for re-use in different scenarios throughout the application. Therefore it
|
||||
is mostly presentational and simply emits values rather than acting on them itself. It is the job of
|
||||
@@ -18,26 +18,66 @@ the parent component to act on those values as needed.
|
||||
|
||||
## `@Input()`'s
|
||||
|
||||
- `email` (**required**) - the parent component must provide an email so that the
|
||||
`InputPasswordComponent` can create a master key.
|
||||
- `buttonText` (optional) - an `i18n` translated string that can be used as button text (default
|
||||
text is "Set master password").
|
||||
- `masterPasswordPolicyOptions` (optional) - used to display and enforce master password policy
|
||||
requirements.
|
||||
**Required**
|
||||
|
||||
- `inputPasswordFlow` - the parent component must provide the correct flow, which is used to
|
||||
determine which form input elements will be displayed in the UI.
|
||||
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
||||
create a master key.
|
||||
|
||||
**Optional**
|
||||
|
||||
- `loading` - a boolean used to indicate that the parent component is performing some
|
||||
long-running/async operation and that the form should be disabled until the operation is complete.
|
||||
The primary button will also show a spinner if `loading` true.
|
||||
- `masterPasswordPolicyOptions` - used to display and enforce master password policy requirements.
|
||||
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
||||
opposed to full-width)
|
||||
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
- `secondaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
|
||||
## `@Output()`'s
|
||||
|
||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||
- `onSecondaryButtonClick` - on click, emits a notice that the secondary button has been clicked.
|
||||
The parent component can listen for this event and take some custom action as needed (go back,
|
||||
cancel, logout, etc.)
|
||||
|
||||
<br />
|
||||
|
||||
## Form Input Fields
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter:
|
||||
The `InputPasswordComponent` can handle up to 6 different form input fields, depending on the
|
||||
`InputPasswordFlow` provided by the parent component.
|
||||
|
||||
1. Master password
|
||||
2. Master password confirmation
|
||||
3. Hint (optional)
|
||||
4. Chooses whether to check for password breaches (checkbox)
|
||||
**InputPasswordFlow.SetInitialPassword**
|
||||
|
||||
Validation ensures that the master password and confirmed master password are the same, and that the
|
||||
master password and hint values are not the same.
|
||||
- Input: New password
|
||||
- Input: Confirm new password
|
||||
- Input: Hint
|
||||
- Checkbox: Check for breaches
|
||||
|
||||
**InputPasswordFlow.ChangePassword**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Input: Current password (as the first element in the UI)
|
||||
|
||||
**InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Checkbox: Rotate account encryption key (as the last element in the UI)
|
||||
|
||||
<br />
|
||||
|
||||
## Validation
|
||||
|
||||
Validation ensures that:
|
||||
|
||||
- The current password and new password are NOT the same
|
||||
- The new password and confirmed new password are the same
|
||||
- The new password and password hint are NOT the same
|
||||
|
||||
<br />
|
||||
|
||||
@@ -57,19 +97,23 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
||||
|
||||
```typescript
|
||||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string; // included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||
}
|
||||
```
|
||||
|
||||
# Default Example
|
||||
# Example - InputPasswordFlow.SetInitialPassword
|
||||
|
||||
<Story of={stories.Default} />
|
||||
<Story of={stories.SetInitialPassword} />
|
||||
|
||||
<br />
|
||||
|
||||
# With Policy Requrements
|
||||
# Example - With Policy Requrements
|
||||
|
||||
<Story of={stories.WithPolicy} />
|
||||
<Story of={stories.WithPolicies} />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
@@ -18,7 +16,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests";
|
||||
|
||||
import { InputPasswordComponent } from "./input-password.component";
|
||||
import { InputPasswordComponent, InputPasswordFlow } from "./input-password.component";
|
||||
|
||||
export default {
|
||||
title: "Auth/Input Password",
|
||||
@@ -62,7 +60,7 @@ export default {
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: {
|
||||
getPasswordStrength: (password) => {
|
||||
let score = 0;
|
||||
let score: number | null = null;
|
||||
if (password.length === 0) {
|
||||
score = null;
|
||||
} else if (password.length <= 4) {
|
||||
@@ -88,6 +86,12 @@ export default {
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
InputPasswordFlow: {
|
||||
SetInitialPassword: InputPasswordFlow.SetInitialPassword,
|
||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||
ChangePasswordWithOptionalUserKeyRotation:
|
||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||
},
|
||||
masterPasswordPolicyOptions: {
|
||||
minComplexity: 4,
|
||||
minLength: 14,
|
||||
@@ -96,25 +100,77 @@ export default {
|
||||
requireNumbers: true,
|
||||
requireSpecial: true,
|
||||
} as MasterPasswordPolicyOptions,
|
||||
argTypes: {
|
||||
onSecondaryButtonClick: { action: "onSecondaryButtonClick" },
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<InputPasswordComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
export const SetInitialPassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password></auth-input-password>
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithPolicy: Story = {
|
||||
export const ChangePassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"></auth-input-password>
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.ChangePassword"></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ChangePasswordWithOptionalUserKeyRotation: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithPolicies: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SecondaryButton: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SecondaryButtonWithPlaceHolderText: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -123,7 +179,24 @@ export const InlineButton: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [btnBlock]="false" [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"></auth-input-password>
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[inlineButtons]="true"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const InlineButtons: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
[inlineButtons]="true"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
password: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
newPassword: "password",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -101,7 +101,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
|
||||
const registerFinishRequest = new RegisterFinishRequest(
|
||||
email,
|
||||
passwordInputResult.masterKeyHash,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.hint,
|
||||
encryptedUserKey,
|
||||
userAsymmetricKeysRequest,
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<auth-input-password
|
||||
*ngIf="!loading"
|
||||
[email]="email"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[loading]="submitting"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
></auth-input-password>
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
PasswordLoginCredentials,
|
||||
} from "../../../common";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { InputPasswordComponent } from "../../input-password/input-password.component";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../../input-password/password-input-result";
|
||||
|
||||
import { RegistrationFinishService } from "./registration-finish.service";
|
||||
@@ -36,6 +39,8 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
InputPasswordFlow = InputPasswordFlow;
|
||||
|
||||
loading = true;
|
||||
submitting = false;
|
||||
email: string;
|
||||
@@ -176,7 +181,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.email,
|
||||
passwordInputResult.password,
|
||||
passwordInputResult.newPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -112,11 +112,11 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
password: "password",
|
||||
newPassword: "password",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
@@ -131,7 +131,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.masterKeyHash,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.hint,
|
||||
orgSsoIdentifier,
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
hint,
|
||||
kdfConfig,
|
||||
@@ -70,7 +70,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterKeyHash,
|
||||
serverMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
hint,
|
||||
orgSsoIdentifier,
|
||||
@@ -92,7 +92,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(masterKeyHash, orgId, userId);
|
||||
await this.handleResetPasswordAutoEnroll(serverMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
[email]="email"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
|
||||
@@ -18,7 +18,10 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "../../../../components/src/toast";
|
||||
import { InputPasswordComponent } from "../input-password/input-password.component";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import {
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
|
||||
@@ -13,7 +13,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { EncryptedObject } from "@bitwarden/common/platform/models/domain/encrypted-object";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
Aes256CbcHmacKey,
|
||||
Aes256CbcKey,
|
||||
SymmetricCryptoKey,
|
||||
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
import { ServerConfig } from "../../../platform/abstractions/config/server-config";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
@@ -46,11 +50,19 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
plainBuf = plainValue;
|
||||
}
|
||||
|
||||
const encObj = await this.aesEncrypt(plainBuf, key);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = encObj.mac != null ? Utils.fromBufferToB64(encObj.mac) : null;
|
||||
return new EncString(encObj.key.encType, data, iv, mac);
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encObj = await this.aesEncrypt(plainBuf, innerKey);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
const mac = Utils.fromBufferToB64(encObj.mac);
|
||||
return new EncString(innerKey.type, data, iv, mac);
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
const encObj = await this.aesEncryptLegacy(plainBuf, innerKey);
|
||||
const iv = Utils.fromBufferToB64(encObj.iv);
|
||||
const data = Utils.fromBufferToB64(encObj.data);
|
||||
return new EncString(innerKey.type, data, iv);
|
||||
}
|
||||
}
|
||||
|
||||
async encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer> {
|
||||
@@ -58,21 +70,26 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
|
||||
const encValue = await this.aesEncrypt(plainValue, key);
|
||||
let macLen = 0;
|
||||
if (encValue.mac != null) {
|
||||
macLen = encValue.mac.byteLength;
|
||||
}
|
||||
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + macLen + encValue.data.byteLength);
|
||||
encBytes.set([encValue.key.encType]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
if (encValue.mac != null) {
|
||||
const innerKey = key.inner();
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encValue = await this.aesEncrypt(plainValue, innerKey);
|
||||
const macLen = encValue.mac.length;
|
||||
const encBytes = new Uint8Array(
|
||||
1 + encValue.iv.byteLength + macLen + encValue.data.byteLength,
|
||||
);
|
||||
encBytes.set([innerKey.type]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
encBytes.set(new Uint8Array(encValue.mac), 1 + encValue.iv.byteLength);
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
const encValue = await this.aesEncryptLegacy(plainValue, innerKey);
|
||||
const encBytes = new Uint8Array(1 + encValue.iv.byteLength + encValue.data.byteLength);
|
||||
encBytes.set([innerKey.type]);
|
||||
encBytes.set(new Uint8Array(encValue.iv), 1);
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
}
|
||||
|
||||
encBytes.set(new Uint8Array(encValue.data), 1 + encValue.iv.byteLength + macLen);
|
||||
return new EncArrayBuffer(encBytes);
|
||||
}
|
||||
|
||||
async decryptToUtf8(
|
||||
@@ -84,36 +101,25 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("No key provided for decryption.");
|
||||
}
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encString?.mac == null) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
const innerKey = key.inner();
|
||||
if (encString.encryptionType !== innerKey.type) {
|
||||
this.logDecryptError(
|
||||
"Key encryption type does not match payload encryption type",
|
||||
key.encType,
|
||||
encString.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encString.encryptionType) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType),
|
||||
"Decrypt context: " + decryptContext,
|
||||
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
encString.mac,
|
||||
key,
|
||||
);
|
||||
if (fastParams.macKey != null && fastParams.mac != null) {
|
||||
const computedMac = await this.cryptoFunctionService.hmacFast(
|
||||
fastParams.macData,
|
||||
fastParams.macKey,
|
||||
@@ -122,18 +128,31 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
|
||||
key.encType,
|
||||
encString.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return await this.cryptoFunctionService.aesDecryptFast({
|
||||
mode: "cbc",
|
||||
parameters: fastParams,
|
||||
});
|
||||
} else if (innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
const fastParams = this.cryptoFunctionService.aesDecryptFastParameters(
|
||||
encString.data,
|
||||
encString.iv,
|
||||
undefined,
|
||||
key,
|
||||
);
|
||||
return await this.cryptoFunctionService.aesDecryptFast({
|
||||
mode: "cbc",
|
||||
parameters: fastParams,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unsupported encryption type`);
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams });
|
||||
}
|
||||
|
||||
async decryptToBytes(
|
||||
@@ -149,72 +168,52 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
throw new Error("Nothing provided for decryption.");
|
||||
}
|
||||
|
||||
// DO NOT REMOVE OR MOVE. This prevents downgrade to mac-less CBC, which would compromise integrity and confidentiality.
|
||||
if (key.macKey != null && encThing.macBytes == null) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
const inner = key.inner();
|
||||
if (encThing.encryptionType !== inner.type) {
|
||||
this.logDecryptError(
|
||||
"Encryption key type mismatch",
|
||||
key.encType,
|
||||
encThing.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.encType !== encThing.encryptionType) {
|
||||
this.logService.error(
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
if (encThing.macBytes == null) {
|
||||
this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (key.macKey != null && encThing.macBytes != null) {
|
||||
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
|
||||
macData.set(new Uint8Array(encThing.ivBytes), 0);
|
||||
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
|
||||
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
|
||||
if (computedMac === null) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
"MAC comparison failed. Key or payload has changed.",
|
||||
key.encType,
|
||||
encThing.encryptionType,
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
key.encKey,
|
||||
"cbc",
|
||||
);
|
||||
} else if (inner.type === EncryptionType.AesCbc256_B64) {
|
||||
return await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
key.encKey,
|
||||
"cbc",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.cryptoFunctionService.aesDecrypt(
|
||||
encThing.dataBytes,
|
||||
encThing.ivBytes,
|
||||
key.encKey,
|
||||
"cbc",
|
||||
);
|
||||
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
@@ -279,25 +278,48 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return Utils.fromBufferToB64(hashArray);
|
||||
}
|
||||
|
||||
private async aesEncrypt(data: Uint8Array, key: SymmetricCryptoKey): Promise<EncryptedObject> {
|
||||
private async aesEncrypt(data: Uint8Array, key: Aes256CbcHmacKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.key = key;
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, obj.key.encKey);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
|
||||
|
||||
if (obj.key.macKey != null) {
|
||||
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, obj.key.macKey, "sha256");
|
||||
}
|
||||
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, key.authenticationKey, "sha256");
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
private logMacFailed(msg: string) {
|
||||
/**
|
||||
* @deprecated Removed once AesCbc256_B64 support is removed
|
||||
*/
|
||||
private async aesEncryptLegacy(data: Uint8Array, key: Aes256CbcKey): Promise<EncryptedObject> {
|
||||
const obj = new EncryptedObject();
|
||||
obj.iv = await this.cryptoFunctionService.randomBytes(16);
|
||||
obj.data = await this.cryptoFunctionService.aesEncrypt(data, obj.iv, key.encryptionKey);
|
||||
return obj;
|
||||
}
|
||||
|
||||
private logDecryptError(
|
||||
msg: string,
|
||||
keyEncType: EncryptionType,
|
||||
dataEncType: EncryptionType,
|
||||
decryptContext: string,
|
||||
) {
|
||||
this.logService.error(
|
||||
`[Encrypt service] ${msg} Key type ${encryptionTypeName(keyEncType)} Payload type ${encryptionTypeName(dataEncType)} Decrypt context: ${decryptContext}`,
|
||||
);
|
||||
}
|
||||
|
||||
private logMacFailed(
|
||||
msg: string,
|
||||
keyEncType: EncryptionType,
|
||||
dataEncType: EncryptionType,
|
||||
decryptContext: string,
|
||||
) {
|
||||
if (this.logMacFailures) {
|
||||
this.logService.error(msg);
|
||||
this.logDecryptError(msg, keyEncType, dataEncType, decryptContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc", async () => {
|
||||
it("decrypts data with provided key for Aes256CbcHmac", async () => {
|
||||
const decryptedBytes = makeStaticByteArray(10, 200);
|
||||
|
||||
cryptoFunctionService.hmac.mockResolvedValue(makeStaticByteArray(1));
|
||||
@@ -257,7 +257,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc_HmacSha256", async () => {
|
||||
it("decrypts data with provided key for AesCbc256_HmacSha256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
|
||||
@@ -277,10 +277,14 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decrypts data with provided key for Aes256Cbc", async () => {
|
||||
it("decrypts data with provided key for AesCbc256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({} as any);
|
||||
cryptoFunctionService.aesDecryptFastParameters.mockReturnValue({
|
||||
macData: makeStaticByteArray(32, 0),
|
||||
macKey: makeStaticByteArray(32, 0),
|
||||
mac: makeStaticByteArray(32, 0),
|
||||
} as any);
|
||||
cryptoFunctionService.hmacFast.mockResolvedValue(makeStaticByteArray(32, 0));
|
||||
cryptoFunctionService.compareFast.mockResolvedValue(true);
|
||||
cryptoFunctionService.aesDecryptFast.mockResolvedValue("data");
|
||||
@@ -290,7 +294,7 @@ describe("EncryptService", () => {
|
||||
expect(cryptoFunctionService.compareFast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null if key is Aes256Cbc_HmacSha256 but EncString is Aes256Cbc", async () => {
|
||||
it("returns null if key is AesCbc256_HMAC but encstring is AesCbc256", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
@@ -299,7 +303,7 @@ describe("EncryptService", () => {
|
||||
expect(logService.error).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null if key is Aes256Cbc but encstring is AesCbc256_HmacSha256", async () => {
|
||||
it("returns null if key is AesCbc256 but encstring is AesCbc256_HMAC", async () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_HmacSha256_B64, "data", "iv", "mac");
|
||||
|
||||
@@ -332,10 +336,7 @@ describe("EncryptService", () => {
|
||||
);
|
||||
});
|
||||
it("returns null if key is mac key but encstring has no mac", async () => {
|
||||
const key = new SymmetricCryptoKey(
|
||||
makeStaticByteArray(64, 0),
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
);
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64, 0));
|
||||
const encString = new EncString(EncryptionType.AesCbc256_B64, "data");
|
||||
|
||||
const actual = await encryptService.decryptToUtf8(encString, key);
|
||||
|
||||
2
libs/common/src/platform/ipc/index.ts
Normal file
2
libs/common/src/platform/ipc/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ipc-message";
|
||||
export * from "./ipc.service";
|
||||
10
libs/common/src/platform/ipc/ipc-message.ts
Normal file
10
libs/common/src/platform/ipc/ipc-message.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
export interface IpcMessage {
|
||||
type: "bitwarden-ipc-message";
|
||||
message: OutgoingMessage;
|
||||
}
|
||||
|
||||
export function isIpcMessage(message: any): message is IpcMessage {
|
||||
return message.type === "bitwarden-ipc-message";
|
||||
}
|
||||
51
libs/common/src/platform/ipc/ipc.service.ts
Normal file
51
libs/common/src/platform/ipc/ipc.service.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { IpcClient, IncomingMessage, OutgoingMessage } from "@bitwarden/sdk-internal";
|
||||
|
||||
export abstract class IpcService {
|
||||
private _client?: IpcClient;
|
||||
protected get client(): IpcClient {
|
||||
if (!this._client) {
|
||||
throw new Error("IpcService not initialized");
|
||||
}
|
||||
return this._client;
|
||||
}
|
||||
|
||||
private _messages$?: Observable<IncomingMessage>;
|
||||
protected get messages$(): Observable<IncomingMessage> {
|
||||
if (!this._messages$) {
|
||||
throw new Error("IpcService not initialized");
|
||||
}
|
||||
return this._messages$;
|
||||
}
|
||||
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
protected async initWithClient(client: IpcClient): Promise<void> {
|
||||
this._client = client;
|
||||
this._messages$ = new Observable<IncomingMessage>((subscriber) => {
|
||||
let isSubscribed = true;
|
||||
|
||||
const receiveLoop = async () => {
|
||||
while (isSubscribed) {
|
||||
try {
|
||||
const message = await this.client.receive();
|
||||
subscriber.next(message);
|
||||
} catch (error) {
|
||||
subscriber.error(error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
void receiveLoop();
|
||||
|
||||
return () => {
|
||||
isSubscribed = false;
|
||||
};
|
||||
}).pipe(shareReplay({ bufferSize: 0, refCount: true }));
|
||||
}
|
||||
|
||||
async send(message: OutgoingMessage) {
|
||||
await this.client.send(message);
|
||||
}
|
||||
}
|
||||
48
libs/common/src/platform/ipc/message-queue.spec.ts
Normal file
48
libs/common/src/platform/ipc/message-queue.spec.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { MessageQueue } from "./message-queue";
|
||||
|
||||
type Message = symbol;
|
||||
|
||||
describe("MessageQueue", () => {
|
||||
let messageQueue!: MessageQueue<Message>;
|
||||
|
||||
beforeEach(() => {
|
||||
messageQueue = new MessageQueue<Message>();
|
||||
});
|
||||
|
||||
it("waits for a new message when queue is empty", async () => {
|
||||
const message = createMessage();
|
||||
|
||||
// Start a promise to dequeue a message
|
||||
let dequeuedValue: Message | undefined;
|
||||
void messageQueue.dequeue().then((value) => {
|
||||
dequeuedValue = value;
|
||||
});
|
||||
|
||||
// No message is enqueued yet
|
||||
expect(dequeuedValue).toBeUndefined();
|
||||
|
||||
// Enqueue a message
|
||||
await messageQueue.enqueue(message);
|
||||
|
||||
// Expect the message to be dequeued
|
||||
await new Promise(process.nextTick);
|
||||
expect(dequeuedValue).toBe(message);
|
||||
});
|
||||
|
||||
it("returns existing message when queue is not empty", async () => {
|
||||
const message = createMessage();
|
||||
|
||||
// Enqueue a message
|
||||
await messageQueue.enqueue(message);
|
||||
|
||||
// Dequeue the message
|
||||
const dequeuedValue = await messageQueue.dequeue();
|
||||
|
||||
// Expect the message to be dequeued
|
||||
expect(dequeuedValue).toBe(message);
|
||||
});
|
||||
});
|
||||
|
||||
function createMessage(name?: string): symbol {
|
||||
return Symbol(name);
|
||||
}
|
||||
20
libs/common/src/platform/ipc/message-queue.ts
Normal file
20
libs/common/src/platform/ipc/message-queue.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
export class MessageQueue<T> {
|
||||
private queue: T[] = [];
|
||||
private messageAvailable$ = new Subject<void>();
|
||||
|
||||
async enqueue(message: T): Promise<void> {
|
||||
this.queue.push(message);
|
||||
this.messageAvailable$.next();
|
||||
}
|
||||
|
||||
async dequeue(): Promise<T> {
|
||||
if (this.queue.length > 0) {
|
||||
return this.queue.shift() as T;
|
||||
}
|
||||
|
||||
await firstValueFrom(this.messageAvailable$);
|
||||
return this.queue.shift() as T;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
|
||||
export class EncryptedObject {
|
||||
iv: Uint8Array;
|
||||
data: Uint8Array;
|
||||
mac: Uint8Array;
|
||||
key: SymmetricCryptoKey;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { makeStaticByteArray } from "../../../../spec";
|
||||
import { EncryptionType } from "../../enums";
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
@@ -24,6 +25,11 @@ describe("SymmetricCryptoKey", () => {
|
||||
key: key,
|
||||
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
|
||||
macKey: null,
|
||||
macKeyB64: undefined,
|
||||
innerKey: {
|
||||
type: EncryptionType.AesCbc256_B64,
|
||||
encryptionKey: key,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +46,11 @@ describe("SymmetricCryptoKey", () => {
|
||||
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
|
||||
macKey: key.slice(32, 64),
|
||||
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
|
||||
innerKey: {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.slice(0, 32),
|
||||
authenticationKey: key.slice(32),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -48,7 +59,7 @@ describe("SymmetricCryptoKey", () => {
|
||||
new SymmetricCryptoKey(makeStaticByteArray(30));
|
||||
};
|
||||
|
||||
expect(t).toThrowError("Unable to determine encType.");
|
||||
expect(t).toThrowError(`Unsupported encType/key length 30`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +80,41 @@ describe("SymmetricCryptoKey", () => {
|
||||
expect(actual).toBeInstanceOf(SymmetricCryptoKey);
|
||||
});
|
||||
|
||||
it("inner returns inner key", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(64));
|
||||
const actual = key.inner();
|
||||
|
||||
expect(actual).toEqual({
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.encKey,
|
||||
authenticationKey: key.macKey,
|
||||
});
|
||||
});
|
||||
|
||||
it("toEncoded returns encoded key for AesCbc256_B64", () => {
|
||||
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
|
||||
const actual = key.toEncoded();
|
||||
|
||||
expect(actual).toEqual(key.encKey);
|
||||
});
|
||||
|
||||
it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => {
|
||||
const keyBytes = makeStaticByteArray(64);
|
||||
const key = new SymmetricCryptoKey(keyBytes);
|
||||
const actual = key.toEncoded();
|
||||
|
||||
expect(actual).toEqual(keyBytes);
|
||||
});
|
||||
|
||||
it("toBase64 returns base64 encoded key", () => {
|
||||
const keyBytes = makeStaticByteArray(64);
|
||||
const keyB64 = Utils.fromBufferToB64(keyBytes);
|
||||
const key = new SymmetricCryptoKey(keyBytes);
|
||||
const actual = key.toBase64();
|
||||
|
||||
expect(actual).toEqual(keyB64);
|
||||
});
|
||||
|
||||
describe("fromString", () => {
|
||||
it("null string returns null", () => {
|
||||
const actual = SymmetricCryptoKey.fromString(null);
|
||||
|
||||
@@ -5,7 +5,25 @@ import { Jsonify } from "type-fest";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { EncryptionType } from "../../enums";
|
||||
|
||||
export type Aes256CbcHmacKey = {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
encryptionKey: Uint8Array;
|
||||
authenticationKey: Uint8Array;
|
||||
};
|
||||
|
||||
export type Aes256CbcKey = {
|
||||
type: EncryptionType.AesCbc256_B64;
|
||||
encryptionKey: Uint8Array;
|
||||
};
|
||||
|
||||
/**
|
||||
* A symmetric crypto key represents a symmetric key usable for symmetric encryption and decryption operations.
|
||||
* The specific algorithm used is private to the key, and should only be exposed to encrypt service implementations.
|
||||
* This can be done via `inner()`.
|
||||
*/
|
||||
export class SymmetricCryptoKey {
|
||||
private innerKey: Aes256CbcHmacKey | Aes256CbcKey;
|
||||
|
||||
key: Uint8Array;
|
||||
encKey: Uint8Array;
|
||||
macKey?: Uint8Array;
|
||||
@@ -17,38 +35,45 @@ export class SymmetricCryptoKey {
|
||||
|
||||
meta: any;
|
||||
|
||||
constructor(key: Uint8Array, encType?: EncryptionType) {
|
||||
/**
|
||||
* @param key The key in one of the permitted serialization formats
|
||||
*/
|
||||
constructor(key: Uint8Array) {
|
||||
if (key == null) {
|
||||
throw new Error("Must provide key");
|
||||
}
|
||||
|
||||
if (encType == null) {
|
||||
if (key.byteLength === 32) {
|
||||
encType = EncryptionType.AesCbc256_B64;
|
||||
} else if (key.byteLength === 64) {
|
||||
encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
} else {
|
||||
throw new Error("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
if (key.byteLength === 32) {
|
||||
this.innerKey = {
|
||||
type: EncryptionType.AesCbc256_B64,
|
||||
encryptionKey: key,
|
||||
};
|
||||
this.encType = EncryptionType.AesCbc256_B64;
|
||||
this.key = key;
|
||||
this.keyB64 = Utils.fromBufferToB64(this.key);
|
||||
|
||||
this.key = key;
|
||||
this.encType = encType;
|
||||
|
||||
if (encType === EncryptionType.AesCbc256_B64 && key.byteLength === 32) {
|
||||
this.encKey = key;
|
||||
this.macKey = null;
|
||||
} else if (encType === EncryptionType.AesCbc256_HmacSha256_B64 && key.byteLength === 64) {
|
||||
this.encKey = key.slice(0, 32);
|
||||
this.macKey = key.slice(32, 64);
|
||||
} else {
|
||||
throw new Error("Unsupported encType/key length.");
|
||||
}
|
||||
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
|
||||
|
||||
this.keyB64 = Utils.fromBufferToB64(this.key);
|
||||
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
|
||||
if (this.macKey != null) {
|
||||
this.macKey = null;
|
||||
this.macKeyB64 = undefined;
|
||||
} else if (key.byteLength === 64) {
|
||||
this.innerKey = {
|
||||
type: EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
encryptionKey: key.slice(0, 32),
|
||||
authenticationKey: key.slice(32),
|
||||
};
|
||||
this.encType = EncryptionType.AesCbc256_HmacSha256_B64;
|
||||
this.key = key;
|
||||
this.keyB64 = Utils.fromBufferToB64(this.key);
|
||||
|
||||
this.encKey = key.slice(0, 32);
|
||||
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
|
||||
|
||||
this.macKey = key.slice(32);
|
||||
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
|
||||
} else {
|
||||
throw new Error(`Unsupported encType/key length ${key.byteLength}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +82,48 @@ export class SymmetricCryptoKey {
|
||||
return { keyB64: this.keyB64 };
|
||||
}
|
||||
|
||||
/**
|
||||
* It is preferred not to work with the raw key where possible.
|
||||
* Only use this method if absolutely necessary.
|
||||
*
|
||||
* @returns The inner key instance that can be directly used for encryption primitives
|
||||
*/
|
||||
inner(): Aes256CbcHmacKey | Aes256CbcKey {
|
||||
return this.innerKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The serialized key in base64 format
|
||||
*/
|
||||
toBase64(): string {
|
||||
return Utils.fromBufferToB64(this.toEncoded());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes the key to a format that can be written to state or shared
|
||||
* The currently permitted format is:
|
||||
* - AesCbc256_B64: 32 bytes (the raw key)
|
||||
* - AesCbc256_HmacSha256_B64: 64 bytes (32 bytes encryption key, 32 bytes authentication key, concatenated)
|
||||
*
|
||||
* @returns The serialized key that can be written to state or encrypted and then written to state / shared
|
||||
*/
|
||||
toEncoded(): Uint8Array {
|
||||
if (this.innerKey.type === EncryptionType.AesCbc256_B64) {
|
||||
return this.innerKey.encryptionKey;
|
||||
} else if (this.innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const encodedKey = new Uint8Array(64);
|
||||
encodedKey.set(this.innerKey.encryptionKey, 0);
|
||||
encodedKey.set(this.innerKey.authenticationKey, 32);
|
||||
return encodedKey;
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param s The serialized key in base64 format
|
||||
* @returns A SymmetricCryptoKey instance
|
||||
*/
|
||||
static fromString(s: string): SymmetricCryptoKey {
|
||||
if (s == null) {
|
||||
return null;
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -24,7 +24,7 @@
|
||||
"@angular/platform-browser": "18.2.13",
|
||||
"@angular/platform-browser-dynamic": "18.2.13",
|
||||
"@angular/router": "18.2.13",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.124",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.133",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "3.0.2",
|
||||
@@ -4699,9 +4699,10 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@bitwarden/sdk-internal": {
|
||||
"version": "0.2.0-main.124",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.124.tgz",
|
||||
"integrity": "sha512-7F+DlPFng/thT4EVIQk2tRC7kff6G2B7alHAIxBdioJc9vE64Z5R5pviUyMZzqLnA5e9y8EnQdtWsQzUkHxisQ=="
|
||||
"version": "0.2.0-main.133",
|
||||
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.133.tgz",
|
||||
"integrity": "sha512-KzKJGf9cKlcQzfRmqkAwVGBN1kDpcRFkTMm7nrphZSrjfaWJWI1lBEJ0DhnkbMMHJXhQavGyoVk5TIn/Y8ylmw==",
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"node_modules/@bitwarden/send-ui": {
|
||||
"resolved": "libs/tools/send/send-ui",
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
"@angular/platform-browser": "18.2.13",
|
||||
"@angular/platform-browser-dynamic": "18.2.13",
|
||||
"@angular/router": "18.2.13",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.124",
|
||||
"@bitwarden/sdk-internal": "0.2.0-main.133",
|
||||
"@electron/fuses": "1.8.0",
|
||||
"@emotion/css": "11.13.5",
|
||||
"@koa/multer": "3.0.2",
|
||||
|
||||
Reference in New Issue
Block a user