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:
commit
9d5dd6567a
9
.github/renovate.json
vendored
9
.github/renovate.json
vendored
@ -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"]
|
||||
|
@ -3107,6 +3107,9 @@
|
||||
"confirmFilePassword": {
|
||||
"message": "Confirm file password"
|
||||
},
|
||||
"exportSuccess": {
|
||||
"message": "Vault data exported"
|
||||
},
|
||||
"typePasskey": {
|
||||
"message": "Passkey"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}));
|
@ -32,7 +32,7 @@ const contentScriptDetails = {
|
||||
...sharedScriptInjectionDetails,
|
||||
};
|
||||
const sharedRegistrationOptions = {
|
||||
matches: ["https://*/*"],
|
||||
matches: ["https://*/*", "http://localhost/*"],
|
||||
excludeMatches: ["https://*/*.xml*"],
|
||||
allFrames: true,
|
||||
...sharedExecuteScriptOptions,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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:",
|
||||
},
|
||||
}));
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -2843,6 +2843,9 @@
|
||||
"confirmFilePassword": {
|
||||
"message": "Confirm file password"
|
||||
},
|
||||
"exportSuccess": {
|
||||
"message": "Vault data exported"
|
||||
},
|
||||
"multifactorAuthenticationCancelled": {
|
||||
"message": "Multifactor authentication cancelled"
|
||||
},
|
||||
|
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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 {}
|
@ -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>
|
@ -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> {}
|
||||
}
|
||||
|
@ -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 {}
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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">×</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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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 {}
|
@ -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>
|
24
apps/web/src/app/tools/vault-export/export-web.component.ts
Normal file
24
apps/web/src/app/tools/vault-export/export-web.component.ts
Normal 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> {}
|
||||
}
|
@ -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>
|
@ -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"),
|
||||
});
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
75
libs/auth/src/angular/input-password/input-password.mdx
Normal file
75
libs/auth/src/angular/input-password/input-password.mdx
Normal 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} />
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
@ -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";
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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", () => {
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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", () => {
|
||||
|
@ -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", () => {
|
||||
|
@ -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
32
package-lock.json
generated
@ -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"
|
||||
},
|
||||
|
@ -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"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user