1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-07 19:07:45 +01:00

Merge branch 'main' into autofill/pm-5189-fix-issues-present-with-inline-menu-rendering-in-iframes

This commit is contained in:
Cesar Gonzalez 2024-06-25 13:38:05 -05:00 committed by GitHub
commit 9d5dd6567a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 669 additions and 1264 deletions

View File

@ -69,14 +69,7 @@
"reviewers": ["team:team-admin-console-dev"]
},
{
"matchPackageNames": [
"@types/duo_web_sdk",
"@types/node-ipc",
"duo_web_sdk",
"node-ipc",
"qrious",
"regedit"
],
"matchPackageNames": ["@types/node-ipc", "node-ipc", "qrious", "regedit"],
"description": "Auth owned dependencies",
"commitMessagePrefix": "[deps] Auth:",
"reviewers": ["team:team-auth-dev"]

View File

@ -3107,6 +3107,9 @@
"confirmFilePassword": {
"message": "Confirm file password"
},
"exportSuccess": {
"message": "Vault data exported"
},
"typePasskey": {
"message": "Passkey"
},

View File

@ -111,7 +111,7 @@
</ng-container>
<!-- Duo -->
<ng-container *ngIf="isDuoProvider">
<div *ngIf="duoFrameless" class="tw-my-4">
<div class="tw-my-4">
<p class="tw-mb-0 tw-text-center">
{{ "duoRequiredForAccount" | i18n }}
</p>
@ -127,17 +127,6 @@
</ng-container>
</div>
<ng-container *ngIf="!duoFrameless">
<div id="duo-frame">
<iframe
id="duo_iframe"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
></iframe>
</div>
<ng-container *ngTemplateOutlet="duoRememberMe"></ng-container>
</ng-container>
<ng-template #duoRememberMe>
<div class="box">
<div class="box-content">
@ -158,7 +147,7 @@
</div>
<!-- Buttons -->
<div class="content no-vpad" *ngIf="selectedProviderType != null">
<ng-container *ngIf="duoFrameless && isDuoProvider">
<ng-container *ngIf="isDuoProvider">
<button
*ngIf="inPopout"
bitButton

View File

@ -1,418 +0,0 @@
/**
* Duo Web SDK v2
* Copyright 2017, Duo Security
*/
var Duo;
(function (root, factory) {
// Browser globals (root is window)
var d = factory();
// If the Javascript was loaded via a script tag, attempt to autoload
// the frame.
d._onReady(d.init);
// Attach Duo to the `window` object
root.Duo = Duo = d;
}(window, function () {
var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/;
var VALID_OPEN_WINDOW_DOMAINS = [
'duo.com',
'duosecurity.com',
'duomobile.s3-us-west-1.amazonaws.com'
];
var iframeId = 'duo_iframe',
postAction = '',
postArgument = 'sig_response',
host,
sigRequest,
duoSig,
appSig,
iframe,
submitCallback;
function throwError(message, url) {
throw new Error(
'Duo Web SDK error: ' + message +
(url ? ('\n' + 'See ' + url + ' for more information') : '')
);
}
function hyphenize(str) {
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
}
// cross-browser data attributes
function getDataAttribute(element, name) {
if ('dataset' in element) {
return element.dataset[name];
} else {
return element.getAttribute('data-' + hyphenize(name));
}
}
// cross-browser event binding/unbinding
function on(context, event, fallbackEvent, callback) {
if ('addEventListener' in window) {
context.addEventListener(event, callback, false);
} else {
context.attachEvent(fallbackEvent, callback);
}
}
function off(context, event, fallbackEvent, callback) {
if ('removeEventListener' in window) {
context.removeEventListener(event, callback, false);
} else {
context.detachEvent(fallbackEvent, callback);
}
}
function onReady(callback) {
on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function offReady(callback) {
off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function onMessage(callback) {
on(window, 'message', 'onmessage', callback);
}
function offMessage(callback) {
off(window, 'message', 'onmessage', callback);
}
/**
* Parse the sig_request parameter, throwing errors if the token contains
* a server error or if the token is invalid.
*
* @param {String} sig Request token
*/
function parseSigRequest(sig) {
if (!sig) {
// nothing to do
return;
}
// see if the token contains an error, throwing it if it does
if (sig.indexOf('ERR|') === 0) {
throwError(sig.split('|')[1]);
}
// validate the token
if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
throwError(
'Duo was given a bad token. This might indicate a configuration ' +
'problem with one of Duo\'s client libraries.',
'https://www.duosecurity.com/docs/duoweb#first-steps'
);
}
var sigParts = sig.split(':');
// hang on to the token, and the parsed duo and app sigs
sigRequest = sig;
duoSig = sigParts[0];
appSig = sigParts[1];
return {
sigRequest: sig,
duoSig: sigParts[0],
appSig: sigParts[1]
};
}
/**
* This function is set up to run when the DOM is ready, if the iframe was
* not available during `init`.
*/
function onDOMReady() {
iframe = document.getElementById(iframeId);
if (!iframe) {
throw new Error(
'This page does not contain an iframe for Duo to use.' +
'Add an element like <iframe id="duo_iframe"></iframe> ' +
'to this page. ' +
'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
'for more information.'
);
}
// we've got an iframe, away we go!
ready();
// always clean up after yourself
offReady(onDOMReady);
}
/**
* Validate that a MessageEvent came from the Duo service, and that it
* is a properly formatted payload.
*
* The Google Chrome sign-in page injects some JS into pages that also
* make use of postMessage, so we need to do additional validation above
* and beyond the origin.
*
* @param {MessageEvent} event Message received via postMessage
*/
function isDuoMessage(event) {
return Boolean(
event.origin === ('https://' + host) &&
typeof event.data === 'string' &&
(
event.data.match(DUO_MESSAGE_FORMAT) ||
event.data.match(DUO_ERROR_FORMAT) ||
event.data.match(DUO_OPEN_WINDOW_FORMAT)
)
);
}
/**
* Validate the request token and prepare for the iframe to become ready.
*
* All options below can be passed into an options hash to `Duo.init`, or
* specified on the iframe using `data-` attributes.
*
* Options specified using the options hash will take precedence over
* `data-` attributes.
*
* Example using options hash:
* ```javascript
* Duo.init({
* iframe: "some_other_id",
* host: "api-main.duo.test",
* sig_request: "...",
* post_action: "/auth",
* post_argument: "resp"
* });
* ```
*
* Example using `data-` attributes:
* ```
* <iframe id="duo_iframe"
* data-host="api-main.duo.test"
* data-sig-request="..."
* data-post-action="/auth"
* data-post-argument="resp"
* >
* </iframe>
* ```
*
* @param {Object} options
* @param {String} options.iframe The iframe, or id of an iframe to set up
* @param {String} options.host Hostname
* @param {String} options.sig_request Request token
* @param {String} [options.post_action=''] URL to POST back to after successful auth
* @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
* @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
* the callback function with reference to the "duo_form" form object
* submit_callback can be used to prevent the webpage from reloading.
*/
function init(options) {
if (options) {
if (options.host) {
host = options.host;
}
if (options.sig_request) {
parseSigRequest(options.sig_request);
}
if (options.post_action) {
postAction = options.post_action;
}
if (options.post_argument) {
postArgument = options.post_argument;
}
if (options.iframe) {
if (options.iframe.tagName) {
iframe = options.iframe;
} else if (typeof options.iframe === 'string') {
iframeId = options.iframe;
}
}
if (typeof options.submit_callback === 'function') {
submitCallback = options.submit_callback;
}
}
// if we were given an iframe, no need to wait for the rest of the DOM
if (false && iframe) {
ready();
} else {
// try to find the iframe in the DOM
iframe = document.getElementById(iframeId);
// iframe is in the DOM, away we go!
if (iframe) {
ready();
} else {
// wait until the DOM is ready, then try again
onReady(onDOMReady);
}
}
// always clean up after yourself!
offReady(init);
}
/**
* This function is called when a message was received from another domain
* using the `postMessage` API. Check that the event came from the Duo
* service domain, and that the message is a properly formatted payload,
* then perform the post back to the primary service.
*
* @param event Event object (contains origin and data)
*/
function onReceivedMessage(event) {
if (isDuoMessage(event)) {
if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) {
var url = event.data.substring("DUO_OPEN_WINDOW|".length);
if (isValidUrlToOpen(url)) {
// Open the URL that comes after the DUO_WINDOW_OPEN token.
window.open(url, "_self");
}
}
else {
// the event came from duo, do the post back
doPostBack(event.data);
// always clean up after yourself!
offMessage(onReceivedMessage);
}
}
}
/**
* Validate that this passed in URL is one that we will actually allow to
* be opened.
* @param url String URL that the message poster wants to open
* @returns {boolean} true if we allow this url to be opened in the window
*/
function isValidUrlToOpen(url) {
if (!url) {
return false;
}
var parser = document.createElement('a');
parser.href = url;
if (parser.protocol === "duotrustedendpoints:") {
return true;
} else if (parser.protocol !== "https:") {
return false;
}
for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) {
if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) ||
parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) {
return true;
}
}
return false;
}
/**
* Point the iframe at Duo, then wait for it to postMessage back to us.
*/
function ready() {
if (!host) {
host = getDataAttribute(iframe, 'host');
if (!host) {
throwError(
'No API hostname is given for Duo to use. Be sure to pass ' +
'a `host` parameter to Duo.init, or through the `data-host` ' +
'attribute on the iframe element.',
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
);
}
}
if (!duoSig || !appSig) {
parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
if (!duoSig || !appSig) {
throwError(
'No valid signed request is given. Be sure to give the ' +
'`sig_request` parameter to Duo.init, or use the ' +
'`data-sig-request` attribute on the iframe element.',
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
);
}
}
// if postAction/Argument are defaults, see if they are specified
// as data attributes on the iframe
if (postAction === '') {
postAction = getDataAttribute(iframe, 'postAction') || postAction;
}
if (postArgument === 'sig_response') {
postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
}
// point the iframe at Duo
iframe.src = [
'https://', host, '/frame/web/v1/auth?tx=', duoSig,
'&parent=', encodeURIComponent(document.location.href),
'&v=2.6'
].join('');
// listen for the 'message' event
onMessage(onReceivedMessage);
}
/**
* We received a postMessage from Duo. POST back to the primary service
* with the response token, and any additional user-supplied parameters
* given in form#duo_form.
*/
function doPostBack(response) {
// create a hidden input to contain the response token
var input = document.createElement('input');
input.type = 'hidden';
input.name = postArgument;
input.value = response + ':' + appSig;
// user may supply their own form with additional inputs
var form = document.getElementById('duo_form');
// if the form doesn't exist, create one
if (!form) {
form = document.createElement('form');
// insert the new form after the iframe
iframe.parentElement.insertBefore(form, iframe.nextSibling);
}
// make sure we are actually posting to the right place
form.method = 'POST';
form.action = postAction;
// add the response token input to the form
form.appendChild(input);
// away we go!
if (typeof submitCallback === "function") {
submitCallback.call(null, form);
} else {
form.submit();
}
}
return {
init: init,
_onReady: onReady,
_parseSigRequest: parseSigRequest,
_isDuoMessage: isDuoMessage,
_doPostBack: doPostBack
};
}));

View File

@ -32,7 +32,7 @@ const contentScriptDetails = {
...sharedScriptInjectionDetails,
};
const sharedRegistrationOptions = {
matches: ["https://*/*"],
matches: ["https://*/*", "http://localhost/*"],
excludeMatches: ["https://*/*.xml*"],
allFrames: true,
...sharedExecuteScriptOptions,

View File

@ -33,7 +33,7 @@ export class Fido2Background implements Fido2BackgroundInterface {
runAt: "document_start",
};
private readonly sharedRegistrationOptions: SharedFido2ScriptRegistrationOptions = {
matches: ["https://*/*"],
matches: ["https://*/*", "http://localhost/*"],
excludeMatches: ["https://*/*.xml*"],
allFrames: true,
...this.sharedInjectionDetails,

View File

@ -17,7 +17,9 @@ import { MessageWithMetadata, Messenger } from "./messaging/messenger";
(function (globalContext) {
const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
globalContext.document.location.protocol === "https:";
(globalContext.document.location.protocol === "https:" ||
(globalContext.document.location.protocol === "http:" &&
globalContext.document.location.hostname === "localhost"));
if (!shouldExecuteContentScript) {
return;

View File

@ -8,7 +8,9 @@ import { Messenger } from "./messaging/messenger";
(function (globalContext) {
const shouldExecuteContentScript =
globalContext.document.contentType === "text/html" &&
globalContext.document.location.protocol === "https:";
(globalContext.document.location.protocol === "https:" ||
(globalContext.document.location.protocol === "http:" &&
globalContext.document.location.hostname === "localhost"));
if (!shouldExecuteContentScript) {
return;

View File

@ -16,8 +16,9 @@ const mockGlobalThisDocument = {
contentType: "text/html",
location: {
...originalGlobalThis.document.location,
href: "https://localhost",
origin: "https://localhost",
href: "https://bitwarden.com",
origin: "https://bitwarden.com",
hostname: "bitwarden.com",
protocol: "https:",
},
};
@ -166,8 +167,8 @@ describe("Fido2 page script with native WebAuthn support", () => {
...mockGlobalThisDocument,
location: {
...mockGlobalThisDocument.location,
href: "http://localhost",
origin: "http://localhost",
href: "http://bitwarden.com",
origin: "http://bitwarden.com",
protocol: "http:",
},
}));

View File

@ -18,7 +18,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.14.1",
"@types/node": "20.14.8",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
}
@ -98,9 +98,9 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA=="
},
"node_modules/@types/node": {
"version": "20.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz",
"integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==",
"version": "20.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"

View File

@ -23,7 +23,7 @@
"yargs": "17.7.2"
},
"devDependencies": {
"@types/node": "20.14.1",
"@types/node": "20.14.8",
"@types/node-ipc": "9.2.3",
"typescript": "4.7.4"
},

View File

@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.6.5",
"version": "2024.6.6",
"keywords": [
"bitwarden",
"password",

View File

@ -90,20 +90,12 @@
<!-- Duo -->
<ng-container *ngIf="isDuoProvider">
<ng-container *ngIf="duoFrameless">
<div>
<span *ngIf="selectedProviderType === providerType.OrganizationDuo" class="tw-mb-0">
{{ "duoRequiredByOrgForAccount" | i18n }}
</span>
{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}
</div>
</ng-container>
<ng-container id="duo-frame" *ngIf="!duoFrameless">
<iframe
id="duo_iframe"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
></iframe>
</ng-container>
<div>
<span *ngIf="selectedProviderType === providerType.OrganizationDuo" class="tw-mb-0">
{{ "duoRequiredByOrgForAccount" | i18n }}
</span>
{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}
</div>
</ng-container>
</div>
@ -148,10 +140,7 @@
<!-- Submit Buttons -->
<div class="buttons with-rows">
<div
class="buttons-row"
*ngIf="duoFrameless && selectedProviderType != null && isDuoProvider"
>
<div class="buttons-row" *ngIf="selectedProviderType != null && isDuoProvider">
<button
(click)="launchDuoFrameless()"
type="button"

View File

@ -2843,6 +2843,9 @@
"confirmFilePassword": {
"message": "Confirm file password"
},
"exportSuccess": {
"message": "Vault data exported"
},
"multifactorAuthenticationCancelled": {
"message": "Multifactor authentication cancelled"
},

View File

@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2024.6.5",
"version": "2024.6.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2024.6.5",
"version": "2024.6.6",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native",

View File

@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2024.6.5",
"version": "2024.6.6",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@ -153,8 +153,17 @@ export class AccountComponent {
}
const request = new OrganizationUpdateRequest();
request.name = this.formGroup.value.orgName;
request.billingEmail = this.formGroup.value.billingEmail;
/*
* When you disable a FormControl, it is removed from formGroup.values, so we have to use
* the original value.
* */
request.name = this.formGroup.get("orgName").disabled
? this.org.name
: this.formGroup.value.orgName;
request.billingEmail = this.formGroup.get("billingEmail").disabled
? this.org.billingEmail
: this.formGroup.value.billingEmail;
// Backfill pub/priv key if necessary
if (!this.org.hasPublicAndPrivateKeys) {

View File

@ -56,12 +56,14 @@ const routes: Routes = [
},
{
path: "export",
loadChildren: () =>
import("../tools/vault-export/org-vault-export.module").then(
(m) => m.OrganizationVaultExportModule,
loadComponent: () =>
import("../tools/vault-export/org-vault-export.component").then(
(mod) => mod.OrganizationVaultExportComponent,
),
canActivate: [OrganizationPermissionsGuard],
data: {
titleId: "exportVault",
organizationPermissions: (org: Organization) => org.canAccessImportExport,
},
},
],

View File

@ -1,25 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationPermissionsGuard } from "../../guards/org-permissions.guard";
import { OrganizationVaultExportComponent } from "./org-vault-export.component";
const routes: Routes = [
{
path: "",
component: OrganizationVaultExportComponent,
canActivate: [OrganizationPermissionsGuard],
data: {
titleId: "exportVault",
organizationPermissions: (org: Organization) => org.canAccessImportExport,
},
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class OrganizationVaultExportRoutingModule {}

View File

@ -0,0 +1,21 @@
<app-header></app-header>
<bit-container>
<tools-export
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulExport)="this.onSuccessfulExport($event)"
organizationId="{{ routeOrgId }}"
></tools-export>
<button
[disabled]="disabled"
[loading]="loading"
form="export_form_exportForm"
bitButton
type="submit"
bitFormButton
buttonType="primary"
>
{{ "confirmFormat" | i18n }}
</button>
</bit-container>

View File

@ -1,83 +1,28 @@
import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { EventType } from "@bitwarden/common/enums";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { ExportComponent } from "@bitwarden/vault-export-ui";
import { ExportComponent } from "../../../../tools/vault-export/export.component";
import { LooseComponentsModule, SharedModule } from "../../../../shared";
@Component({
selector: "app-org-export",
templateUrl: "../../../../tools/vault-export/export.component.html",
templateUrl: "org-vault-export.component.html",
standalone: true,
imports: [SharedModule, ExportComponent, LooseComponentsModule],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class OrganizationVaultExportComponent extends ExportComponent {
constructor(
i18nService: I18nService,
toastService: ToastService,
exportService: VaultExportServiceAbstraction,
eventCollectionService: EventCollectionService,
private route: ActivatedRoute,
policyService: PolicyService,
logService: LogService,
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
) {
super(
i18nService,
toastService,
exportService,
eventCollectionService,
policyService,
logService,
formBuilder,
fileDownloadService,
dialogService,
organizationService,
);
}
export class OrganizationVaultExportComponent implements OnInit {
protected routeOrgId: string = null;
protected loading = false;
protected disabled = false;
protected get disabledByPolicy(): boolean {
return false;
}
constructor(private route: ActivatedRoute) {}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
});
await super.ngOnInit();
this.routeOrgId = this.route.snapshot.paramMap.get("organizationId");
}
getExportData() {
return this.exportService.getOrganizationExport(
this.organizationId,
this.format,
this.filePassword,
);
}
getFileName() {
return super.getFileName("org");
}
async collectEvent(): Promise<void> {
await this.eventCollectionService.collect(
EventType.Organization_ClientExportedVault,
null,
null,
this.organizationId,
);
}
/**
* Callback that is called after a successful export.
*/
protected async onSuccessfulExport(organizationId: string): Promise<void> {}
}

View File

@ -1,19 +0,0 @@
import { NgModule } from "@angular/core";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { LooseComponentsModule, SharedModule } from "../../../../shared";
import { OrganizationVaultExportRoutingModule } from "./org-vault-export-routing.module";
import { OrganizationVaultExportComponent } from "./org-vault-export.component";
@NgModule({
imports: [
SharedModule,
LooseComponentsModule,
OrganizationVaultExportRoutingModule,
ExportScopeCalloutComponent,
],
declarations: [OrganizationVaultExportComponent],
})
export class OrganizationVaultExportModule {}

View File

@ -5,17 +5,11 @@ import { Subject, takeUntil } from "rxjs";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import {
Argon2KdfConfig,
DEFAULT_KDF_CONFIG,
KdfConfig,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import {
DEFAULT_KDF_CONFIG,
PBKDF2_ITERATIONS,
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
KdfType,
} from "@bitwarden/common/platform/enums";
import { KdfType } from "@bitwarden/common/platform/enums";
import { DialogService } from "@bitwarden/components";
import { ChangeKdfConfirmationComponent } from "./change-kdf-confirmation.component";
@ -36,30 +30,34 @@ export class ChangeKdfComponent implements OnInit {
this.kdfConfig.iterations,
[
Validators.required,
Validators.min(PBKDF2_ITERATIONS.min),
Validators.max(PBKDF2_ITERATIONS.max),
Validators.min(PBKDF2KdfConfig.ITERATIONS.min),
Validators.max(PBKDF2KdfConfig.ITERATIONS.max),
],
],
memory: [
null as number,
[Validators.required, Validators.min(ARGON2_MEMORY.min), Validators.max(ARGON2_MEMORY.max)],
[
Validators.required,
Validators.min(Argon2KdfConfig.MEMORY.min),
Validators.max(Argon2KdfConfig.MEMORY.max),
],
],
parallelism: [
null as number,
[
Validators.required,
Validators.min(ARGON2_PARALLELISM.min),
Validators.max(ARGON2_PARALLELISM.max),
Validators.min(Argon2KdfConfig.PARALLELISM.min),
Validators.max(Argon2KdfConfig.PARALLELISM.max),
],
],
}),
});
// Default values for template
protected PBKDF2_ITERATIONS = PBKDF2_ITERATIONS;
protected ARGON2_ITERATIONS = ARGON2_ITERATIONS;
protected ARGON2_MEMORY = ARGON2_MEMORY;
protected ARGON2_PARALLELISM = ARGON2_PARALLELISM;
protected PBKDF2_ITERATIONS = PBKDF2KdfConfig.ITERATIONS;
protected ARGON2_ITERATIONS = Argon2KdfConfig.ITERATIONS;
protected ARGON2_MEMORY = Argon2KdfConfig.MEMORY;
protected ARGON2_PARALLELISM = Argon2KdfConfig.PARALLELISM;
constructor(
private dialogService: DialogService,
@ -97,26 +95,26 @@ export class ChangeKdfComponent implements OnInit {
config = new PBKDF2KdfConfig();
validators.iterations = [
Validators.required,
Validators.min(PBKDF2_ITERATIONS.min),
Validators.max(PBKDF2_ITERATIONS.max),
Validators.min(PBKDF2KdfConfig.ITERATIONS.min),
Validators.max(PBKDF2KdfConfig.ITERATIONS.max),
];
break;
case KdfType.Argon2id:
config = new Argon2KdfConfig();
validators.iterations = [
Validators.required,
Validators.min(ARGON2_ITERATIONS.min),
Validators.max(ARGON2_ITERATIONS.max),
Validators.min(Argon2KdfConfig.ITERATIONS.min),
Validators.max(Argon2KdfConfig.ITERATIONS.max),
];
validators.memory = [
Validators.required,
Validators.min(ARGON2_MEMORY.min),
Validators.max(ARGON2_MEMORY.max),
Validators.min(Argon2KdfConfig.MEMORY.min),
Validators.max(Argon2KdfConfig.MEMORY.max),
];
validators.parallelism = [
Validators.required,
Validators.min(ARGON2_PARALLELISM.min),
Validators.max(ARGON2_PARALLELISM.max),
Validators.min(Argon2KdfConfig.PARALLELISM.min),
Validators.max(Argon2KdfConfig.PARALLELISM.max),
];
break;
default:

View File

@ -59,8 +59,8 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
protected async enable() {
const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest);
request.integrationKey = this.clientId;
request.secretKey = this.clientSecret;
request.clientId = this.clientId;
request.clientSecret = this.clientSecret;
request.host = this.host;
return super.enable(async () => {
@ -78,8 +78,8 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent {
}
private processResponse(response: TwoFactorDuoResponse) {
this.clientId = response.integrationKey;
this.clientSecret = response.secretKey;
this.clientId = response.clientId;
this.clientSecret = response.clientSecret;
this.host = response.host;
this.enabled = response.enabled;
}

View File

@ -39,8 +39,6 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
@ViewChild("duoTemplate", { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
@ViewChild("emailTemplate", { read: ViewContainerRef, static: true })
emailModalRef: ViewContainerRef;
@ViewChild("webAuthnTemplate", { read: ViewContainerRef, static: true })
webAuthnModalRef: ViewContainerRef;
organizationId: string;
organization: Organization;
@ -192,12 +190,11 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy {
if (!result) {
return;
}
const webAuthnComp = await this.openModal(
this.webAuthnModalRef,
TwoFactorWebAuthnComponent,
const webAuthnComp: DialogRef<boolean, any> = TwoFactorWebAuthnComponent.open(
this.dialogService,
{ data: result },
);
webAuthnComp.auth(result);
webAuthnComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
webAuthnComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.WebAuthn);
});
break;

View File

@ -1,152 +1,118 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="2faU2fTitle">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title" id="2faU2fTitle">
{{ "twoStepLogin" | i18n }}
<small>{{ "webAuthnTitle" | i18n }}</small>
</h1>
<button
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="authed"
<form *ngIf="authed" [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog dialogSize="large">
<span bitDialogTitle>
{{ "twoStepLogin" | i18n }}
<span bitTypography="body1">{{ "webAuthnTitle" | i18n }}</span>
</span>
<ng-container bitDialogContent>
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="enabled"
>
<div class="modal-body">
<app-callout
type="success"
title="{{ 'enabled' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="enabled"
>
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p>{{ "twoFactorWebAuthnWarning" | i18n }}</p>
<ul class="mb-0">
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
</ul>
</app-callout>
<img class="float-right ml-5 mfaType7" alt="FIDO2 WebAuthn logo'" />
<ul class="bwi-ul">
<li
*ngFor="let k of keys; let i = index"
#removeKeyBtn
[appApiAction]="k.removePromise"
>
<i class="bwi bwi-li bwi-key"></i>
<strong *ngIf="!k.configured || !k.name">{{ "webAuthnkeyX" | i18n: i + 1 }}</strong>
<strong *ngIf="k.configured && k.name">{{ k.name }}</strong>
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
<ng-container *ngIf="k.migrated">
<span>{{ "webAuthnMigrated" | i18n }}</span>
</ng-container>
</ng-container>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i
class="bwi bwi-spin bwi-spinner text-muted bwi-fw"
title="{{ 'loading' | i18n }}"
*ngIf="$any(removeKeyBtn).loading"
aria-hidden="true"
></i>
-
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
</ng-container>
</li>
</ul>
<hr />
<p>{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
<ol>
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
</ol>
<div class="row">
<div class="form-group col-6">
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
class="form-control"
[(ngModel)]="name"
[disabled]="!keyIdAvailable"
/>
</div>
</div>
<button
type="button"
(click)="readKey()"
class="btn btn-outline-secondary mr-2"
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
#readKeyBtn
[appApiAction]="challengePromise"
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="$any(readKeyBtn).loading">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!$any(readKeyBtn).loading">
<ng-container *ngIf="webAuthnListening">
<i class="bwi bwi-spinner bwi-spin text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="bwi bwi-check-circle text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="bwi bwi-exclamation-triangle text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
{{ "twoStepLoginProviderEnabled" | i18n }}
</app-callout>
<app-callout type="warning">
<p bitTypography="body1">{{ "twoFactorWebAuthnWarning" | i18n }}</p>
<ul class="tw-mb-0">
<li>{{ "twoFactorWebAuthnSupportWeb" | i18n }}</li>
</ul>
</app-callout>
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
<ul class="bwi-ul">
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
<i class="bwi bwi-li bwi-key"></i>
<span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-bold">
{{ "webAuthnkeyX" | i18n: i + 1 }}
</span>
<span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-bold">
{{ k.name }}
</span>
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
<ng-container *ngIf="k.migrated">
<span>{{ "webAuthnMigrated" | i18n }}</span>
</ng-container>
</ng-container>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary"
[disabled]="form.loading || !webAuthnResponse"
>
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
<i
class="bwi bwi-spinner bwi-spin"
*ngIf="form.loading"
class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
title="{{ 'loading' | i18n }}"
*ngIf="$any(removeKeyBtn).loading"
aria-hidden="true"
></i>
<span *ngIf="!form.loading">{{ "save" | i18n }}</span>
</button>
<button
#disableBtn
type="button"
class="btn btn-outline-secondary btn-submit"
[disabled]="$any(disableBtn).loading"
(click)="disable()"
*ngIf="enabled"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "disableAllKeys" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</form>
</div>
</div>
</div>
-
<a href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
</ng-container>
</li>
</ul>
<hr />
<p bitTypography="body1">{{ "twoFactorWebAuthnAdd" | i18n }}:</p>
<ol bitTypography="body1">
<li>{{ "twoFactorU2fGiveName" | i18n }}</li>
<li>{{ "twoFactorU2fPlugInReadKey" | i18n }}</li>
<li>{{ "twoFactorU2fTouchButton" | i18n }}</li>
<li>{{ "twoFactorU2fSaveForm" | i18n }}</li>
</ol>
<div class="tw-flex">
<bit-form-field class="tw-basis-1/2">
<bit-label>{{ "name" | i18n }}</bit-label>
<input bitInput type="text" formControlName="name" />
</bit-form-field>
</div>
<button
bitButton
bitFormButton
type="button"
[bitAction]="readKey"
buttonType="secondary"
[disabled]="$any(readKeyBtn).loading || webAuthnListening || !keyIdAvailable"
class="tw-mr-2"
#readKeyBtn
>
{{ "readKey" | i18n }}
</button>
<ng-container *ngIf="$any(readKeyBtn).loading">
<i class="bwi bwi-spinner bwi-spin tw-text-muted" aria-hidden="true"></i>
</ng-container>
<ng-container *ngIf="!$any(readKeyBtn).loading">
<ng-container *ngIf="webAuthnListening">
<i class="bwi bwi-spinner bwi-spin tw-text-muted" aria-hidden="true"></i>
{{ "twoFactorU2fWaiting" | i18n }}...
</ng-container>
<ng-container *ngIf="webAuthnResponse">
<i class="bwi bwi-check-circle tw-text-success" aria-hidden="true"></i>
{{ "twoFactorU2fClickSave" | i18n }}
</ng-container>
<ng-container *ngIf="webAuthnError">
<i class="bwi bwi-exclamation-triangle tw-text-danger" aria-hidden="true"></i>
{{ "twoFactorU2fProblemReadingTryAgain" | i18n }}
</ng-container>
</ng-container>
</ng-container>
<ng-container bitDialogFooter>
<button
bitButton
bitFormButton
type="submit"
buttonType="primary"
[disabled]="!webAuthnResponse"
>
{{ "save" | i18n }}
</button>
<button
bitButton
bitFormButton
*ngIf="enabled"
type="button"
buttonType="secondary"
[bitAction]="disable"
>
{{ "disableAllKeys" | i18n }}
</button>
<button bitButton bitFormButton type="button" buttonType="secondary" bitDialogClose>
{{ "close" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -1,4 +1,6 @@
import { Component, NgZone } from "@angular/core";
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, EventEmitter, Inject, NgZone, Output } from "@angular/core";
import { FormControl, FormGroup } from "@angular/forms";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
@ -31,6 +33,7 @@ interface Key {
templateUrl: "two-factor-webauthn.component.html",
})
export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
@Output() onChangeStatus = new EventEmitter<boolean>();
type = TwoFactorProviderType.WebAuthn;
name: string;
keys: Key[];
@ -44,7 +47,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
override componentName = "app-two-factor-webauthn";
protected formGroup = new FormGroup({
name: new FormControl({ value: "", disabled: !this.keyIdAvailable }),
});
constructor(
@Inject(DIALOG_DATA) protected data: AuthResponse<TwoFactorWebAuthnResponse>,
private dialogRef: DialogRef,
apiService: ApiService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
@ -61,6 +70,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
userVerificationService,
dialogService,
);
this.auth(data);
}
auth(authResponse: AuthResponse<TwoFactorWebAuthnResponse>) {
@ -68,7 +78,7 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
this.processResponse(authResponse.response);
}
async submit() {
submit = async () => {
if (this.webAuthnResponse == null || this.keyIdAvailable == null) {
// Should never happen.
return Promise.reject();
@ -76,16 +86,28 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
const request = await this.buildRequestModel(UpdateTwoFactorWebAuthnRequest);
request.deviceResponse = this.webAuthnResponse;
request.id = this.keyIdAvailable;
request.name = this.name;
request.name = this.formGroup.value.name;
return this.enableWebAuth(request);
};
private enableWebAuth(request: any) {
return super.enable(async () => {
this.formPromise = this.apiService.putTwoFactorWebAuthn(request);
const response = await this.formPromise;
await this.processResponse(response);
this.processResponse(response);
});
}
disable() {
disable = async () => {
await this.disableWebAuth();
if (!this.enabled) {
this.onChangeStatus.emit(this.enabled);
this.dialogRef.close();
}
};
private async disableWebAuth() {
return super.disable(this.formPromise);
}
@ -116,19 +138,15 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
}
}
async readKey() {
readKey = async () => {
if (this.keyIdAvailable == null) {
return;
}
const request = await this.buildRequestModel(SecretVerificationRequest);
try {
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
} catch (e) {
this.logService.error(e);
}
}
this.challengePromise = this.apiService.getTwoFactorWebAuthnChallenge(request);
const challenge = await this.challengePromise;
this.readDevice(challenge);
};
private readDevice(webAuthnChallenge: ChallengeResponse) {
// eslint-disable-next-line
@ -164,7 +182,8 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
this.resetWebAuthn();
this.keys = [];
this.keyIdAvailable = null;
this.name = null;
this.formGroup.get("name").enable();
this.formGroup.get("name").setValue(null);
this.keysConfiguredCount = 0;
for (let i = 1; i <= 5; i++) {
if (response.keys != null) {
@ -187,5 +206,13 @@ export class TwoFactorWebAuthnComponent extends TwoFactorBaseComponent {
}
}
this.enabled = response.enabled;
this.onChangeStatus.emit(this.enabled);
}
static open(
dialogService: DialogService,
config: DialogConfig<AuthResponse<TwoFactorWebAuthnResponse>>,
) {
return dialogService.open<boolean>(TwoFactorWebAuthnComponent, config);
}
}

View File

@ -54,25 +54,14 @@
</ng-container>
<!-- Duo -->
<ng-container *ngIf="isDuoProvider">
<ng-container *ngIf="duoFrameless">
<p
bitTypography="body1"
*ngIf="selectedProviderType === providerType.OrganizationDuo"
class="tw-mb-0"
>
{{ "duoRequiredByOrgForAccount" | i18n }}
</p>
<p bitTypography="body1">{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
</ng-container>
<ng-container *ngIf="!duoFrameless">
<div id="duo-frame" class="tw-mb-3">
<iframe
id="duo_iframe"
sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-popups-to-escape-sandbox"
></iframe>
</div>
</ng-container>
<p
bitTypography="body1"
*ngIf="selectedProviderType === providerType.OrganizationDuo"
class="tw-mb-0"
>
{{ "duoRequiredByOrgForAccount" | i18n }}
</p>
<p bitTypography="body1">{{ "launchDuoAndFollowStepsToFinishLoggingIn" | i18n }}</p>
</ng-container>
<bit-form-control *ngIf="selectedProviderType != null">
<bit-label>{{ "rememberMe" | i18n }}</bit-label>
@ -107,7 +96,7 @@
buttonType="primary"
bitButton
bitFormButton
*ngIf="duoFrameless && isDuoProvider"
*ngIf="isDuoProvider"
>
<span> {{ "launchDuo" | i18n }} </span>
</button>

View File

@ -448,8 +448,13 @@ const routes: Routes = [
},
{
path: "export",
loadChildren: () =>
import("./tools/vault-export/export.module").then((m) => m.ExportModule),
loadComponent: () =>
import("./tools/vault-export/export-web.component").then(
(mod) => mod.ExportWebComponent,
),
data: {
titleId: "exportVault",
} satisfies DataProperties,
},
{
path: "generator",

View File

@ -1,17 +0,0 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { ExportComponent } from "./export.component";
const routes: Routes = [
{
path: "",
component: ExportComponent,
data: { titleId: "exportVault" },
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class ExportRoutingModule {}

View File

@ -0,0 +1,20 @@
<app-header></app-header>
<bit-container>
<tools-export
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulExport)="this.onSuccessfulExport($event)"
></tools-export>
<button
[disabled]="disabled"
[loading]="loading"
form="export_form_exportForm"
bitButton
type="submit"
bitFormButton
buttonType="primary"
>
{{ "confirmFormat" | i18n }}
</button>
</bit-container>

View File

@ -0,0 +1,24 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { ExportComponent } from "@bitwarden/vault-export-ui";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
@Component({
templateUrl: "export-web.component.html",
standalone: true,
imports: [SharedModule, ExportComponent, HeaderModule],
})
export class ExportWebComponent {
protected loading = false;
protected disabled = false;
constructor(private router: Router) {}
/**
* Callback that is called after a successful export.
*/
protected async onSuccessfulExport(organizationId: string): Promise<void> {}
}

View File

@ -1,115 +0,0 @@
<app-header></app-header>
<bit-container>
<form [formGroup]="exportForm" [bitSubmit]="submit">
<bit-callout type="danger" title="{{ 'vaultExportDisabled' | i18n }}" *ngIf="disabledByPolicy">
{{ "personalVaultExportPolicyInEffect" | i18n }}
</bit-callout>
<tools-export-scope-callout
[organizationId]="organizationId"
*ngIf="!disabledByPolicy"
></tools-export-scope-callout>
<ng-container *ngIf="organizations$ | async as organizations">
<bit-form-field *ngIf="organizations.length > 0">
<bit-label>{{ "exportFrom" | i18n }}</bit-label>
<bit-select formControlName="vaultSelector">
<bit-option [label]="'myVault' | i18n" value="myVault" icon="bwi-user" />
<bit-option
*ngFor="let o of organizations$ | async"
[value]="o.id"
[label]="o.name"
icon="bwi-business"
/>
</bit-select>
</bit-form-field>
</ng-container>
<bit-form-field>
<bit-label>{{ "fileFormat" | i18n }}</bit-label>
<bit-select formControlName="format">
<bit-option *ngFor="let f of formatOptions" [value]="f.value" [label]="f.name" />
</bit-select>
</bit-form-field>
<ng-container *ngIf="format === 'encrypted_json'">
<bit-radio-group formControlName="fileEncryptionType" aria-label="exportTypeHeading">
<bit-label>{{ "exportTypeHeading" | i18n }}</bit-label>
<bit-radio-button
id="AccountEncrypted"
name="fileEncryptionType"
class="tw-block"
[value]="encryptedExportType.AccountEncrypted"
checked="fileEncryptionType === encryptedExportType.AccountEncrypted"
>
<bit-label>{{ "accountRestricted" | i18n }}</bit-label>
<bit-hint>{{ "accountRestrictedOptionDescription" | i18n }}</bit-hint>
</bit-radio-button>
<bit-radio-button
id="FileEncrypted"
name="fileEncryptionType"
class="tw-block"
[value]="encryptedExportType.FileEncrypted"
checked="fileEncryptionType === encryptedExportType.FileEncrypted"
>
<bit-label>{{ "passwordProtected" | i18n }}</bit-label>
<bit-hint>{{ "passwordProtectedOptionDescription" | i18n }}</bit-hint>
</bit-radio-button>
</bit-radio-group>
<ng-container *ngIf="fileEncryptionType == encryptedExportType.FileEncrypted">
<div class="tw-mb-3">
<bit-form-field>
<bit-label>{{ "filePassword" | i18n }}</bit-label>
<input
bitInput
type="password"
id="filePassword"
formControlName="filePassword"
name="password"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showFilePassword"
></button>
<bit-hint>{{ "exportPasswordDescription" | i18n }}</bit-hint>
</bit-form-field>
<app-password-strength [password]="filePassword" [showText]="true">
</app-password-strength>
</div>
<bit-form-field>
<bit-label>{{ "confirmFilePassword" | i18n }}</bit-label>
<input
bitInput
type="password"
id="confirmFilePassword"
formControlName="confirmFilePassword"
name="confirmFilePassword"
/>
<button
type="button"
bitSuffix
bitIconButton
bitPasswordInputToggle
[(toggled)]="showFilePassword"
></button>
</bit-form-field>
</ng-container>
</ng-container>
<button
bitButton
bitFormButton
type="submit"
buttonType="primary"
[disabled]="disabledByPolicy"
>
{{ "confirmFormat" | i18n }}
</button>
</form>
</bit-container>

View File

@ -1,53 +0,0 @@
import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
import { ExportComponent as BaseExportComponent } from "@bitwarden/vault-export-ui";
@Component({
selector: "app-export",
templateUrl: "export.component.html",
})
export class ExportComponent extends BaseExportComponent {
constructor(
i18nService: I18nService,
toastService: ToastService,
exportService: VaultExportServiceAbstraction,
eventCollectionService: EventCollectionService,
policyService: PolicyService,
logService: LogService,
formBuilder: UntypedFormBuilder,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
organizationService: OrganizationService,
) {
super(
i18nService,
toastService,
exportService,
eventCollectionService,
policyService,
logService,
formBuilder,
fileDownloadService,
dialogService,
organizationService,
);
}
protected saved() {
super.saved();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("exportSuccess"),
});
}
}

View File

@ -1,14 +0,0 @@
import { NgModule } from "@angular/core";
import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui";
import { LooseComponentsModule, SharedModule } from "../../shared";
import { ExportRoutingModule } from "./export-routing.module";
import { ExportComponent } from "./export.component";
@NgModule({
imports: [SharedModule, LooseComponentsModule, ExportRoutingModule, ExportScopeCalloutComponent],
declarations: [ExportComponent],
})
export class ExportModule {}

View File

@ -5,9 +5,10 @@ import { mergeMap, take } from "rxjs/operators";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
import { KdfType } from "@bitwarden/common/platform/enums";
import {
StateProvider,
ActiveUserState,
@ -200,7 +201,7 @@ export class VaultBannersService {
const kdfConfig = await this.kdfConfigService.getKdfConfig();
return (
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue
kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue
);
}

View File

@ -1,17 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width"
/>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; child-src 'self' https://*.duosecurity.com https://*.duofederal.com; frame-src 'self' https://*.duosecurity.com https://*.duofederal.com;"
/>
<title>Bitwarden Duo Connector</title>
</head>
<body></body>
</html>

View File

@ -1,18 +0,0 @@
html,
body {
margin: 0;
padding: 0;
}
body {
background: #efeff4 url("../images/loading.svg") 0 0 no-repeat;
}
iframe {
display: block;
width: 100%;
height: 400px;
border: none;
margin: 0;
padding: 0;
}

View File

@ -1,47 +0,0 @@
import * as DuoWebSDK from "duo_web_sdk";
import { getQsParam } from "./common";
require("./duo.scss");
document.addEventListener("DOMContentLoaded", () => {
const frameElement = document.createElement("iframe");
frameElement.setAttribute("id", "duo_iframe");
setFrameHeight();
document.body.appendChild(frameElement);
const hostParam = getQsParam("host");
const requestParam = getQsParam("request");
const hostUrl = new URL("https://" + hostParam);
if (
!hostUrl.hostname.endsWith(".duosecurity.com") &&
!hostUrl.hostname.endsWith(".duofederal.com")
) {
return;
}
DuoWebSDK.init({
iframe: "duo_iframe",
host: hostUrl.hostname,
sig_request: requestParam,
submit_callback: (form: any) => {
invokeCSCode(form.elements.sig_response.value);
},
});
window.onresize = setFrameHeight;
function setFrameHeight() {
frameElement.style.height = window.innerHeight + "px";
}
});
function invokeCSCode(data: string) {
try {
(window as any).invokeCSharpAction(data);
} catch (err) {
// eslint-disable-next-line
console.log(err);
}
}

View File

@ -91,11 +91,6 @@ const plugins = [
chunks: ["theme_head", "app/polyfills", "app/vendor", "app/main"],
}),
new HtmlWebpackInjector(),
new HtmlWebpackPlugin({
template: "./src/connectors/duo.html",
filename: "duo-connector.html",
chunks: ["connectors/duo"],
}),
new HtmlWebpackPlugin({
template: "./src/connectors/webauthn.html",
filename: "webauthn-connector.html",
@ -324,7 +319,6 @@ const webpackConfig = {
"app/main": "./src/main.ts",
"connectors/webauthn": "./src/connectors/webauthn.ts",
"connectors/webauthn-fallback": "./src/connectors/webauthn-fallback.ts",
"connectors/duo": "./src/connectors/duo.ts",
"connectors/sso": "./src/connectors/sso.ts",
"connectors/captcha": "./src/connectors/captcha.ts",
"connectors/duo-redirect": "./src/connectors/duo-redirect.ts",

View File

@ -0,0 +1,37 @@
import { AbstractControl, ValidationErrors } from "@angular/forms";
import { domainNameValidator } from "./domain-name.validator";
describe("domainNameValidator", () => {
let validatorFn: (control: AbstractControl) => ValidationErrors | null;
const errorMessage = "Invalid domain name";
beforeEach(() => {
validatorFn = domainNameValidator(errorMessage);
});
const testCases = [
{ value: "e.com", expected: null },
{ value: "example.com", expected: null },
{ value: "sub.example.com", expected: null },
{ value: "sub.sub.example.com", expected: null },
{ value: "example.co.uk", expected: null },
{ value: "example", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "-example.com", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "example-.com", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "example..com", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "http://example.com", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "www.example.com", expected: { invalidDomainName: { message: errorMessage } } },
{ value: "", expected: null },
{ value: "x".repeat(64) + ".com", expected: { invalidDomainName: { message: errorMessage } } },
];
describe("run test cases", () => {
testCases.forEach(({ value, expected }) => {
test(`should return ${JSON.stringify(expected)} for value "${value}"`, () => {
const control = { value } as AbstractControl;
expect(validatorFn(control)).toEqual(expected);
});
});
});
});

View File

@ -13,24 +13,22 @@ export function domainNameValidator(errorMessage: string): ValidatorFn {
// We do not want any prefixes per industry standards.
// Must support top-level domains and any number of subdomains.
// / # start regex
// ^ # start of string
// (?!(http(s)?:\/\/|www\.)) # negative lookahead to check if input doesn't match "http://", "https://" or "www."
// [a-zA-Z0-9] # first character must be a letter or a number
// [a-zA-Z0-9-]{0,61} # domain name can have 0 to 61 characters that are letters, numbers, or hyphens
// [a-zA-Z0-9] # domain name must end with a letter or a number
// (?: # start of non-capturing group (subdomain sections are optional)
// \. # subdomain must have a period
// [a-zA-Z0-9] # first character of subdomain must be a letter or a number
// [a-zA-Z0-9-]{0,61} # subdomain can have 0 to 61 characters that are letters, numbers, or hyphens
// [a-zA-Z0-9] # subdomain must end with a letter or a number
// )* # end of non-capturing group (subdomain sections are optional)
// \. # domain name must have a period
// [a-zA-Z]{2,} # domain name must have at least two letters (the domain extension)
// $/ # end of string
// / # start regex
// ^ # start of string
// (?!(http(s)?:\/\/|www\.)) # negative lookahead to check if input doesn't match "http://", "https://" or "www."
// ( # start of capturing group for the entire domain
// [a-zA-Z0-9] # first character of domain must be a letter or a number
// ( # start of optional group for subdomain or domain section
// [a-zA-Z0-9-]{0,61} # subdomain/domain section can have 0 to 61 characters that are letters, numbers, or hyphens
// [a-zA-Z0-9] # subdomain/domain section must end with a letter or a number
// )? # end of optional group for subdomain or domain section
// \. # subdomain/domain section must have a period
// )+ # end of capturing group for the entire domain, repeatable for subdomains
// [a-zA-Z]{2,} # domain name must have at least two letters (the domain extension)
// $/ # end of string
const validDomainNameRegex =
/^(?!(http(s)?:\/\/|www\.))[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9](?:\.[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9])*\.[a-zA-Z]{2,}$/;
/^(?!(http(s)?:\/\/|www\.))([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
const invalid = !validDomainNameRegex.test(control.value);

View File

@ -5,6 +5,7 @@ import { Router } from "@angular/router";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { RegisterResponse } from "@bitwarden/common/auth/models/response/register.response";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
@ -15,7 +16,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "@bitwarden/components";

View File

@ -17,6 +17,7 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -24,7 +25,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { HashPurpose, DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";

View File

@ -1,6 +1,5 @@
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import * as DuoWebSDK from "duo_web_sdk";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
@ -53,7 +52,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
emailPromise: Promise<any>;
orgIdentifier: string = null;
duoFrameless = false;
duoFramelessUrl: string = null;
duoResultListenerInitialized = false;
@ -177,42 +175,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
break;
case TwoFactorProviderType.Duo:
case TwoFactorProviderType.OrganizationDuo:
// 2 Duo 2FA flows available
// 1. Duo Web SDK (iframe) - existing, to be deprecated
// 2. Duo Frameless (new tab) - new
// AuthUrl only exists for new Duo Frameless flow
if (providerData.AuthUrl) {
this.duoFrameless = true;
// Setup listener for duo-redirect.ts connector to send back the code
if (!this.duoResultListenerInitialized) {
// setup client specific duo result listener
this.setupDuoResultListener();
this.duoResultListenerInitialized = true;
}
// flow must be launched by user so they can choose to remember the device or not.
this.duoFramelessUrl = providerData.AuthUrl;
} else {
// Duo Web SDK (iframe) flow
// TODO: remove when we remove the "duo-redirect" feature flag
setTimeout(() => {
DuoWebSDK.init({
iframe: undefined,
host: providerData.Host,
sig_request: providerData.Signature,
submit_callback: async (f: HTMLFormElement) => {
const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement;
if (sig != null) {
this.token = sig.value;
await this.submit();
}
},
});
}, 0);
// Setup listener for duo-redirect.ts connector to send back the code
if (!this.duoResultListenerInitialized) {
// setup client specific duo result listener
this.setupDuoResultListener();
this.duoResultListenerInitialized = true;
}
// flow must be launched by user so they can choose to remember the device or not.
this.duoFramelessUrl = providerData.AuthUrl;
break;
case TwoFactorProviderType.Email:
this.twoFactorEmail = providerData.Email;

View File

@ -6,10 +6,12 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
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";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import {
DEFAULT_KDF_CONFIG,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MasterKey } from "@bitwarden/common/types/key";
import {

View File

@ -0,0 +1,75 @@
import { Meta, Story } from "@storybook/addon-docs";
import * as stories from "./input-password.stories.ts";
<Meta of={stories} />
# 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 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
the parent component to act on those values as needed.
<br />
## `@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").
- `orgId` (optional) - used to retreive and enforce the master password policy requirements for an
org.
<br />
## Form Input Fields
The `InputPasswordComponent` allows a user to enter:
1. Master password
2. Master password confirmation
3. Hint (optional)
4. Chooses whether to check for password breaches (checkbox)
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.
<br />
## On Submit
When the form is submitted, the `InputPasswordComponent` does the following in order:
1. If the user selected the checkbox to check for password breaches, they will recieve a popup
dialog if their entered password is found in a breach. The popup will give them the option to
continue with the password or to back out and choose a different password.
2. If there is a master password policy being enforced by an org, it will check to make sure the
entered master password meets the policy requirements.
3. The component will use the password, email, and default kdfConfig to create a master key and
master key hash.
4. The component will emit the following values (defined in the `PasswordInputResult` interface) to
be used by the parent component as needed:
```typescript
export interface PasswordInputResult {
masterKey: MasterKey;
masterKeyHash: string;
kdfConfig: PBKDF2KdfConfig;
hint: string;
}
```
# Default Example
<Story of={stories.Default} />
<br />
# With Policy Requrements
<Story of={stories.WithPolicy} />

View File

@ -29,7 +29,10 @@ const mockMasterPasswordPolicyOptions = {
export default {
title: "Auth/Input Password",
component: InputPasswordComponent,
decorators: [
} as Meta;
const decorators = (options: { hasPolicy?: boolean }) => {
return [
applicationConfig({
providers: [
importProvidersFrom(PreloadedEnglishI18nModule),
@ -56,13 +59,15 @@ export default {
{
provide: PolicyApiServiceAbstraction,
useValue: {
getMasterPasswordPolicyOptsForOrgUser: () => mockMasterPasswordPolicyOptions,
getMasterPasswordPolicyOptsForOrgUser: () =>
options.hasPolicy ? mockMasterPasswordPolicyOptions : null,
} as Partial<PolicyService>,
},
{
provide: PolicyService,
useValue: {
masterPasswordPolicyOptions$: () => of(mockMasterPasswordPolicyOptions),
masterPasswordPolicyOptions$: () =>
options.hasPolicy ? of(mockMasterPasswordPolicyOptions) : null,
evaluateMasterPassword: (score) => {
if (score < 4) {
return false;
@ -101,8 +106,8 @@ export default {
},
],
}),
],
} as Meta;
];
};
type Story = StoryObj<InputPasswordComponent>;
@ -113,4 +118,19 @@ export const Default: Story = {
<auth-input-password></auth-input-password>
`,
}),
decorators: decorators({
hasPolicy: false,
}),
};
export const WithPolicy: Story = {
render: (args) => ({
props: args,
template: `
<auth-input-password></auth-input-password>
`,
}),
decorators: decorators({
hasPolicy: true,
}),
};

View File

@ -1,13 +1,13 @@
import { mock } from "jest-mock-extended";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";

View File

@ -1,12 +1,7 @@
import { Jsonify } from "type-fest";
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
KdfType,
PBKDF2_ITERATIONS,
} from "../../../platform/enums/kdf-type.enum";
import { KdfType } from "../../../platform/enums/kdf-type.enum";
import { RangeWithDefault } from "../../../platform/misc/range-with-default";
/**
* Represents a type safe KDF configuration.
@ -17,11 +12,12 @@ export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig;
* Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration.
*/
export class PBKDF2KdfConfig {
static ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000);
kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256;
iterations: number;
constructor(iterations?: number) {
this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue;
this.iterations = iterations ?? PBKDF2KdfConfig.ITERATIONS.defaultValue;
}
/**
@ -29,9 +25,9 @@ export class PBKDF2KdfConfig {
* A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000.
*/
validateKdfConfig(): void {
if (!PBKDF2_ITERATIONS.inRange(this.iterations)) {
if (!PBKDF2KdfConfig.ITERATIONS.inRange(this.iterations)) {
throw new Error(
`PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`,
`PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`,
);
}
}
@ -45,15 +41,18 @@ export class PBKDF2KdfConfig {
* Argon2 KDF configuration.
*/
export class Argon2KdfConfig {
static MEMORY = new RangeWithDefault(16, 1024, 64);
static PARALLELISM = new RangeWithDefault(1, 16, 4);
static ITERATIONS = new RangeWithDefault(2, 10, 3);
kdfType: KdfType.Argon2id = KdfType.Argon2id;
iterations: number;
memory: number;
parallelism: number;
constructor(iterations?: number, memory?: number, parallelism?: number) {
this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue;
this.memory = memory ?? ARGON2_MEMORY.defaultValue;
this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue;
this.iterations = iterations ?? Argon2KdfConfig.ITERATIONS.defaultValue;
this.memory = memory ?? Argon2KdfConfig.MEMORY.defaultValue;
this.parallelism = parallelism ?? Argon2KdfConfig.PARALLELISM.defaultValue;
}
/**
@ -61,21 +60,21 @@ export class Argon2KdfConfig {
* A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16.
*/
validateKdfConfig(): void {
if (!ARGON2_ITERATIONS.inRange(this.iterations)) {
if (!Argon2KdfConfig.ITERATIONS.inRange(this.iterations)) {
throw new Error(
`Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`,
`Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`,
);
}
if (!ARGON2_MEMORY.inRange(this.memory)) {
if (!Argon2KdfConfig.MEMORY.inRange(this.memory)) {
throw new Error(
`Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`,
`Argon2 memory must be between ${Argon2KdfConfig.MEMORY.min}mb and ${Argon2KdfConfig.MEMORY.max}mb`,
);
}
if (!ARGON2_PARALLELISM.inRange(this.parallelism)) {
if (!Argon2KdfConfig.PARALLELISM.inRange(this.parallelism)) {
throw new Error(
`Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`,
`Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}.`,
);
}
}
@ -84,3 +83,5 @@ export class Argon2KdfConfig {
return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism);
}
}
export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2KdfConfig.ITERATIONS.defaultValue);

View File

@ -1,7 +1,7 @@
import { SecretVerificationRequest } from "./secret-verification.request";
export class UpdateTwoFactorDuoRequest extends SecretVerificationRequest {
integrationKey: string;
secretKey: string;
clientId: string;
clientSecret: string;
host: string;
}

View File

@ -3,14 +3,14 @@ import { BaseResponse } from "../../../models/response/base.response";
export class TwoFactorDuoResponse extends BaseResponse {
enabled: boolean;
host: string;
secretKey: string;
integrationKey: string;
clientSecret: string;
clientId: string;
constructor(response: any) {
super(response);
this.enabled = this.getResponseProperty("Enabled");
this.host = this.getResponseProperty("Host");
this.secretKey = this.getResponseProperty("SecretKey");
this.integrationKey = this.getResponseProperty("IntegrationKey");
this.clientSecret = this.getResponseProperty("ClientSecret");
this.clientId = this.getResponseProperty("ClientId");
}
}

View File

@ -1,10 +1,4 @@
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
PBKDF2_ITERATIONS,
} from "../../platform/enums/kdf-type.enum";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config";
@ -77,28 +71,28 @@ describe("KdfConfigService", () => {
it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => {
const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`,
`PBKDF2 iterations must be between ${PBKDF2KdfConfig.ITERATIONS.min} and ${PBKDF2KdfConfig.ITERATIONS.max}`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`,
`Argon2 iterations must be between ${Argon2KdfConfig.ITERATIONS.min} and ${Argon2KdfConfig.ITERATIONS.max}`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`,
`Argon2 memory must be between ${Argon2KdfConfig.MEMORY.min}mb and ${Argon2KdfConfig.MEMORY.max}mb`,
);
});
it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => {
const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17);
expect(() => kdfConfig.validateKdfConfig()).toThrow(
`Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`,
`Argon2 parallelism must be between ${Argon2KdfConfig.PARALLELISM.min} and ${Argon2KdfConfig.PARALLELISM.max}`,
);
});
});

View File

@ -1,15 +1,4 @@
import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config";
import { RangeWithDefault } from "../misc/range-with-default";
export enum KdfType {
PBKDF2_SHA256 = 0,
Argon2id = 1,
}
export const ARGON2_MEMORY = new RangeWithDefault(16, 1024, 64);
export const ARGON2_PARALLELISM = new RangeWithDefault(1, 16, 4);
export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3);
export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256;
export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000);
export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue);

View File

@ -19,6 +19,7 @@ import {
} from "../../abstractions/fido2/fido2-client.service.abstraction";
import { Utils } from "../../misc/utils";
import * as DomainUtils from "./domain-utils";
import { Fido2AuthenticatorService } from "./fido2-authenticator.service";
import { Fido2ClientService } from "./fido2-client.service";
import { Fido2Utils } from "./fido2-utils";
@ -36,6 +37,7 @@ describe("FidoAuthenticatorService", () => {
let domainSettingsService: MockProxy<DomainSettingsService>;
let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab;
let isValidRpId!: jest.SpyInstance;
beforeEach(async () => {
authenticator = mock<Fido2AuthenticatorService>();
@ -44,6 +46,8 @@ describe("FidoAuthenticatorService", () => {
vaultSettingsService = mock<VaultSettingsService>();
domainSettingsService = mock<DomainSettingsService>();
isValidRpId = jest.spyOn(DomainUtils, "isValidRpId");
client = new Fido2ClientService(
authenticator,
configService,
@ -58,6 +62,10 @@ describe("FidoAuthenticatorService", () => {
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
});
afterEach(() => {
isValidRpId.mockRestore();
});
describe("createCredential", () => {
describe("input parameters validation", () => {
// Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
@ -113,6 +121,7 @@ describe("FidoAuthenticatorService", () => {
});
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm.
// This is actually checked by `isValidRpId` function, but we'll test it here as well
it("should throw error if rp.id is not valid for this origin", async () => {
const params = createParams({
origin: "https://passwordless.dev",
@ -126,6 +135,20 @@ describe("FidoAuthenticatorService", () => {
await rejects.toBeInstanceOf(DOMException);
});
// Sanity check to make sure that we use `isValidRpId` to validate the rp.id
it("should throw if isValidRpId returns false", async () => {
const params = createParams();
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.createCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should fallback if origin hostname is found in neverDomains", async () => {
const params = createParams({
origin: "https://bitwarden.com",
@ -151,6 +174,16 @@ describe("FidoAuthenticatorService", () => {
await rejects.toBeInstanceOf(DOMException);
});
it("should not throw error if localhost is http", async () => {
const params = createParams({
origin: "http://localhost",
rp: { id: undefined, name: "localhost" },
});
authenticator.makeCredential.mockResolvedValue(createAuthenticatorMakeResult());
await client.createCredential(params, tab);
});
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
it("should throw error if no support key algorithms were found", async () => {
const params = createParams({
@ -360,6 +393,7 @@ describe("FidoAuthenticatorService", () => {
});
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain, return a DOMException whose name is "SecurityError", and terminate this algorithm.
// This is actually checked by `isValidRpId` function, but we'll test it here as well
it("should throw error if rp.id is not valid for this origin", async () => {
const params = createParams({
origin: "https://passwordless.dev",
@ -373,6 +407,20 @@ describe("FidoAuthenticatorService", () => {
await rejects.toBeInstanceOf(DOMException);
});
// Sanity check to make sure that we use `isValidRpId` to validate the rp.id
it("should throw if isValidRpId returns false", async () => {
const params = createParams();
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
// `params` actually has a valid rp.id, but we're mocking the function to return false
isValidRpId.mockReturnValue(false);
const result = async () => await client.assertCredential(params, tab);
const rejects = expect(result).rejects;
await rejects.toMatchObject({ name: "SecurityError" });
await rejects.toBeInstanceOf(DOMException);
});
it("should fallback if origin hostname is found in neverDomains", async () => {
const params = createParams({
origin: "https://bitwarden.com",
@ -506,6 +554,16 @@ describe("FidoAuthenticatorService", () => {
expect.anything(),
);
});
it("should not throw error if localhost is http", async () => {
const params = createParams({
origin: "http://localhost",
});
params.rpId = undefined;
authenticator.getAssertion.mockResolvedValue(createAuthenticatorAssertResult());
await client.assertCredential(params, tab);
});
});
describe("assert discoverable credential", () => {

View File

@ -103,7 +103,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
}
params.rp.id = params.rp.id ?? parsedOrigin.hostname;
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
if (
parsedOrigin.hostname == undefined ||
(!params.origin.startsWith("https://") && parsedOrigin.hostname !== "localhost")
) {
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
}
@ -238,7 +241,10 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
params.rpId = params.rpId ?? parsedOrigin.hostname;
if (parsedOrigin.hostname == undefined || !params.origin.startsWith("https://")) {
if (
parsedOrigin.hostname == undefined ||
(!params.origin.startsWith("https://") && parsedOrigin.hostname !== "localhost")
) {
this.logService?.warning(`[Fido2Client] Invalid https origin: ${params.origin}`);
throw new DOMException("'origin' is not a valid https origin", "SecurityError");
}

View File

@ -1,14 +1,8 @@
import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config";
import { CsprngArray } from "../../types/csprng";
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "../abstractions/key-generation.service";
import {
ARGON2_ITERATIONS,
ARGON2_MEMORY,
ARGON2_PARALLELISM,
KdfType,
PBKDF2_ITERATIONS,
} from "../enums";
import { KdfType } from "../enums";
import { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@ -51,21 +45,21 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
let key: Uint8Array = null;
if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue;
kdfConfig.iterations = PBKDF2KdfConfig.ITERATIONS.defaultValue;
}
key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations);
} else if (kdfConfig.kdfType == KdfType.Argon2id) {
if (kdfConfig.iterations == null) {
kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue;
kdfConfig.iterations = Argon2KdfConfig.ITERATIONS.defaultValue;
}
if (kdfConfig.memory == null) {
kdfConfig.memory = ARGON2_MEMORY.defaultValue;
kdfConfig.memory = Argon2KdfConfig.MEMORY.defaultValue;
}
if (kdfConfig.parallelism == null) {
kdfConfig.parallelism = ARGON2_PARALLELISM.defaultValue;
kdfConfig.parallelism = Argon2KdfConfig.PARALLELISM.defaultValue;
}
const saltHash = await this.cryptoFunctionService.hash(salt, "sha256");

View File

@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import {
DEFAULT_KDF_CONFIG,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -238,7 +242,7 @@ describe("VaultExportService", () => {
});
it("specifies kdfIterations", () => {
expect(exportObject.kdfIterations).toEqual(PBKDF2_ITERATIONS.defaultValue);
expect(exportObject.kdfIterations).toEqual(PBKDF2KdfConfig.ITERATIONS.defaultValue);
});
it("has kdfType", () => {

View File

@ -2,10 +2,14 @@ import { mock, MockProxy } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import {
DEFAULT_KDF_CONFIG,
PBKDF2KdfConfig,
} from "@bitwarden/common/auth/models/domain/kdf-config";
import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums";
import { KdfType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -238,7 +242,7 @@ describe("VaultExportService", () => {
});
it("specifies kdfIterations", () => {
expect(exportObject.kdfIterations).toEqual(PBKDF2_ITERATIONS.defaultValue);
expect(exportObject.kdfIterations).toEqual(PBKDF2KdfConfig.ITERATIONS.defaultValue);
});
it("has kdfType", () => {

View File

@ -1,5 +1,13 @@
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from "@angular/core";
import {
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms";
import { map, merge, Observable, startWith, Subject, takeUntil } from "rxjs";
@ -53,6 +61,26 @@ import { ExportScopeCalloutComponent } from "./export-scope-callout.component";
],
})
export class ExportComponent implements OnInit, OnDestroy {
private _organizationId: string;
get organizationId(): string {
return this._organizationId;
}
/**
* Enables the hosting control to pass in an organizationId
* If a organizationId is provided, the organization selection is disabled.
*/
@Input() set organizationId(value: string) {
this._organizationId = value;
this.organizationService
.get$(this._organizationId)
.pipe(takeUntil(this.destroy$))
.subscribe((organization) => {
this._organizationId = organization?.id;
});
}
/**
* The hosting control also needs a bitSubmitDirective (on the Submit button) which calls this components {@link submit}-method.
* This components formState (loading/disabled) is emitted back up to the hosting component so for example the Submit button can be enabled/disabled and show loading state.
@ -82,7 +110,6 @@ export class ExportComponent implements OnInit, OnDestroy {
@Output()
onSuccessfulExport = new EventEmitter<string>();
@Output() onSaved = new EventEmitter();
@ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent;
encryptedExportType = EncryptedExportType;
@ -91,7 +118,6 @@ export class ExportComponent implements OnInit, OnDestroy {
filePasswordValue: string = null;
private _disabledByPolicy = false;
protected organizationId: string = null;
organizations$: Observable<Organization[]>;
protected get disabledByPolicy(): boolean {
@ -120,6 +146,7 @@ export class ExportComponent implements OnInit, OnDestroy {
];
private destroy$ = new Subject<void>();
private onlyManagedCollections = true;
constructor(
protected i18nService: I18nService,
@ -163,6 +190,8 @@ export class ExportComponent implements OnInit, OnDestroy {
);
this.exportForm.controls.vaultSelector.patchValue(this.organizationId);
this.exportForm.controls.vaultSelector.disable();
this.onlyManagedCollections = false;
return;
}
@ -211,7 +240,12 @@ export class ExportComponent implements OnInit, OnDestroy {
try {
const data = await this.getExportData();
this.downloadFile(data);
this.saved();
this.toastService.showToast({
variant: "success",
title: null,
message: this.i18nService.t("exportSuccess"),
});
this.onSuccessfulExport.emit(this.organizationId);
await this.collectEvent();
this.exportForm.get("secret").setValue("");
this.exportForm.clearValidators();
@ -252,11 +286,6 @@ export class ExportComponent implements OnInit, OnDestroy {
await this.doExport();
};
protected saved() {
this.onSaved.emit();
this.onSuccessfulExport.emit(this.organizationId);
}
private async verifyUser(): Promise<boolean> {
let confirmDescription = "exportWarningDesc";
if (this.isFileEncryptedExport) {
@ -298,7 +327,7 @@ export class ExportComponent implements OnInit, OnDestroy {
this.organizationId,
this.format,
this.filePassword,
true,
this.onlyManagedCollections,
);
}

32
package-lock.json generated
View File

@ -39,7 +39,6 @@
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.36.1",
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
"form-data": "4.0.0",
"https-proxy-agent": "7.0.2",
"inquirer": "8.2.6",
@ -68,7 +67,7 @@
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.29",
"utf-8-validate": "6.0.3",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"
},
@ -97,7 +96,6 @@
"@storybook/testing-library": "0.2.2",
"@types/argon2-browser": "1.18.1",
"@types/chrome": "0.0.262",
"@types/duo_web_sdk": "2.7.1",
"@types/firefox-webext-browser": "111.0.5",
"@types/inquirer": "8.2.10",
"@types/jest": "29.5.12",
@ -110,7 +108,7 @@
"@types/koa-json": "2.0.23",
"@types/lowdb": "1.0.15",
"@types/lunr": "2.3.7",
"@types/node": "20.14.1",
"@types/node": "20.14.8",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
@ -237,7 +235,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
"version": "2024.6.5",
"version": "2024.6.6",
"hasInstallScript": true,
"license": "GPL-3.0"
},
@ -11352,12 +11350,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/duo_web_sdk": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@types/duo_web_sdk/-/duo_web_sdk-2.7.1.tgz",
"integrity": "sha512-DePanZjFww36yGSxXwC8B3AsjrrDuPxEcufeh4gTqVsUMpCYByxjX4PERiYZdW0typzKSt9E4I14PPp+PrSIQA==",
"dev": true
},
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
@ -11750,9 +11742,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.1.tgz",
"integrity": "sha512-T2MzSGEu+ysB/FkWfqmhV3PLyQlowdptmmgD20C6QxsS8Fmv5SjpZ1ayXaEC0S21/h5UJ9iA6W/5vSNU5l00OA==",
"version": "20.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz",
"integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -18249,11 +18241,6 @@
"node": ">=12"
}
},
"node_modules/duo_web_sdk": {
"version": "2.7.0",
"resolved": "git+ssh://git@github.com/duosecurity/duo_web_sdk.git#29cad7338eff2cd909a361ecdd525458862938be",
"license": "SEE LICENSE IN LICENSE"
},
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@ -38757,10 +38744,11 @@
}
},
"node_modules/utf-8-validate": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.3.tgz",
"integrity": "sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==",
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz",
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-gyp-build": "^4.3.0"
},

View File

@ -58,7 +58,6 @@
"@storybook/testing-library": "0.2.2",
"@types/argon2-browser": "1.18.1",
"@types/chrome": "0.0.262",
"@types/duo_web_sdk": "2.7.1",
"@types/firefox-webext-browser": "111.0.5",
"@types/inquirer": "8.2.10",
"@types/jest": "29.5.12",
@ -71,7 +70,7 @@
"@types/koa-json": "2.0.23",
"@types/lowdb": "1.0.15",
"@types/lunr": "2.3.7",
"@types/node": "20.14.1",
"@types/node": "20.14.8",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "1.3.11",
"@types/node-ipc": "9.2.3",
@ -176,7 +175,6 @@
"chalk": "4.1.2",
"commander": "11.1.0",
"core-js": "3.36.1",
"duo_web_sdk": "github:duosecurity/duo_web_sdk",
"form-data": "4.0.0",
"https-proxy-agent": "7.0.2",
"inquirer": "8.2.6",
@ -205,7 +203,7 @@
"rxjs": "7.8.1",
"tabbable": "6.2.0",
"tldts": "6.1.29",
"utf-8-validate": "6.0.3",
"utf-8-validate": "6.0.4",
"zone.js": "0.13.3",
"zxcvbn": "4.4.2"
},