Merge remote-tracking branch 'origin/main' into auth/pm-7392/token-service-add-secure-storage-fallback + app.component merge conflict resolution

This commit is contained in:
Jared Snider 2024-05-06 11:19:11 -04:00
commit ec01624f81
No known key found for this signature in database
GPG Key ID: A149DDD612516286
99 changed files with 521 additions and 360 deletions

View File

@ -226,7 +226,7 @@
"message": "Help & feedback"
},
"helpCenter": {
"message": "Bitwarden Help center"
"message": "Bitwarden Help centre"
},
"communityForums": {
"message": "Explore Bitwarden community forums"
@ -728,7 +728,7 @@
"message": "Change the application's colour theme."
},
"themeDescAlt": {
"message": "Change the application's color theme. Applies to all logged in accounts."
"message": "Change the application's colour theme. Applies to all logged in accounts."
},
"dark": {
"message": "Dark",
@ -1165,7 +1165,7 @@
"message": "Show a recognizable image next to each login."
},
"faviconDescAlt": {
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
"message": "Show a recognisable image next to each login. Applies to all logged in accounts."
},
"enableBadgeCounter": {
"message": "Show badge counter"
@ -1730,7 +1730,7 @@
"message": "An organization policy is affecting your ownership options."
},
"personalOwnershipPolicyInEffectImports": {
"message": "An organization policy has blocked importing items into your individual vault."
"message": "An organisation policy has blocked importing items into your individual vault."
},
"excludedDomains": {
"message": "Excluded Domains"
@ -1990,7 +1990,7 @@
"message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
"message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic Enrollment"
@ -2006,11 +2006,11 @@
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "Your organisation permissions were updated, requiring you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Your organisation requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
@ -2037,7 +2037,7 @@
}
},
"vaultTimeoutPolicyWithActionInEffect": {
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"placeholders": {
"hours": {
"content": "$1",
@ -2054,7 +2054,7 @@
}
},
"vaultTimeoutActionPolicyInEffect": {
"message": "Your organization policies have set your vault timeout action to $ACTION$.",
"message": "Your organisation policies have set your vault timeout action to $ACTION$.",
"placeholders": {
"action": {
"content": "$1",
@ -2111,7 +2111,7 @@
"message": "Exporting Personal Vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
@ -2305,7 +2305,7 @@
}
},
"autofillPageLoadPolicyActivated": {
"message": "Your organization policies have turned on auto-fill on page load."
"message": "Your organisation policies have turned on auto-fill on page load."
},
"howToAutofill": {
"message": "How to auto-fill"
@ -2377,7 +2377,7 @@
"message": "Approve with master password"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
"message": "Organisation SSO identifier is required."
},
"eu": {
"message": "EU",
@ -2688,7 +2688,7 @@
"message": "Total"
},
"importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?",
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?",
"placeholders": {
"organization": {
"content": "$1",
@ -3007,10 +3007,10 @@
"message": "Passkey removed"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console."
},
"unassignedItemsBannerSelfHostNotice": {
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
"message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",

View File

@ -3,7 +3,7 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden - Administrador de contraseñas",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
@ -2962,27 +2962,27 @@
"description": "Label indicating the most common import formats"
},
"overrideDefaultBrowserAutofillTitle": {
"message": "¿Quiere hacer de Bitwarden su gestor de contraseñas predeterminado?",
"message": "¿Hacer de Bitwarden su administrador de contraseñas predeterminado?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillDescription": {
"message": "Pasar por alto esta opción puede causar conflictos entre el menú de relleno automático de Bitwarden y el del navegador.",
"message": "Pasar por alto esta opción puede causar conflictos entre el menú de autocompletar de Bitwarden y el de tu navegador.",
"description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutoFillSettings": {
"message": "Hacer de Bitwarden su gestor de contraseñas predeterminado",
"message": "Hacer de Bitwarden tu administrador de contraseñas predeterminado",
"description": "Label for the setting that allows overriding the default browser autofill settings"
},
"privacyPermissionAdditionNotGrantedTitle": {
"message": "No se pudo establecer Bitwarden como el gestor de contraseñas predeterminado",
"message": "No se puede establecer Bitwarden como el administrador de contraseñas predeterminado",
"description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"privacyPermissionAdditionNotGrantedDescription": {
"message": "Debe otorgar los permisos de privacidad del navegador a Bitwarden para establecerlo como gestor de contraseñas predeterminado.",
"message": "Debes otorgar permisos de privacidad del navegador a Bitwarden para establecerlo como administrador de contraseñas predeterminado.",
"description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"makeDefault": {
"message": "Predeterminar",
"message": "Establecer como predeterminado",
"description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
@ -2998,7 +2998,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Éxito"
},
"removePasskey": {
"message": "Eliminar passkey"
@ -3013,20 +3013,20 @@
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Asignar estos elementos a una colección de",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"unassignedItemsBannerCTAPartTwo": {
"message": "to make them visible.",
"message": "para hcerlos visibles.",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"adminConsole": {
"message": "Admin Console"
"message": "Consola de administrador"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Error al asignar la colección de destino."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Error al asignar la carpeta de destino."
}
}

View File

@ -173,10 +173,10 @@
"message": "Keisti pagrindinį slaptažodį"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Tęsti į žiniatinklio programėlę?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Pagrindinį slaptažodį galite pakeisti „Bitwarden“ žiniatinklio programėlėje."
},
"fingerprintPhrase": {
"message": "Pirštų atspaudų frazė",
@ -2998,7 +2998,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Sėkmė"
},
"removePasskey": {
"message": "Pašalinti slaptaraktį"
@ -3007,26 +3007,26 @@
"message": "Pašalintas slaptaraktis"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę."
},
"unassignedItemsBannerSelfHostNotice": {
"message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console."
"message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Priskirkite šiuos elementus kolekcijai iš",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"unassignedItemsBannerCTAPartTwo": {
"message": "to make them visible.",
"message": ", kad jie būtų matomi.",
"description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible."
},
"adminConsole": {
"message": "Admin Console"
"message": "Administratoriaus konsolės"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Klaida priskiriant tikslinę kolekciją."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Klaida priskiriant tikslinį aplanką."
}
}

View File

@ -1,4 +1,4 @@
import { Subject, firstValueFrom, map, merge, timeout } from "rxjs";
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
import {
PinCryptoServiceAbstraction,
@ -1201,31 +1201,46 @@ export default class MainBackground {
}
async logout(expired: boolean, userId?: UserId) {
userId ??= (
await firstValueFrom(
this.accountService.activeAccount$.pipe(
timeout({
first: 2000,
with: () => {
throw new Error("No active account found to logout");
},
}),
),
)
)?.id;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(
map((a) => a?.id),
timeout({
first: 2000,
with: () => {
throw new Error("No active account found to logout");
},
}),
),
);
await this.eventUploadService.uploadEvents(userId as UserId);
const userBeingLoggedOut = userId ?? activeUserId;
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
// HACK: We shouldn't wait for the authentication status to change but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error("The logout process did not complete in a reasonable amount of time.");
},
}),
),
);
await Promise.all([
this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.passwordGenerationService.clear(userId),
this.vaultTimeoutSettingsService.clear(userId),
this.syncService.setLastSync(new Date(0), userBeingLoggedOut),
this.cryptoService.clearKeys(userBeingLoggedOut),
this.cipherService.clear(userBeingLoggedOut),
this.folderService.clear(userBeingLoggedOut),
this.collectionService.clear(userBeingLoggedOut),
this.passwordGenerationService.clear(userBeingLoggedOut),
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
this.vaultFilterService.clear(),
this.biometricStateService.logout(userId),
this.biometricStateService.logout(userBeingLoggedOut),
/* We intentionally do not clear:
* - autofillSettingsService
* - badgeSettingsService
@ -1236,20 +1251,28 @@ export default class MainBackground {
//Needs to be checked before state is cleaned
const needStorageReseed = await this.needsStorageReseed();
const newActiveUser = await firstValueFrom(
this.accountService.nextUpAccount$.pipe(map((a) => a?.id)),
);
await this.stateService.clean({ userId: userId });
await this.accountService.clean(userId);
const newActiveUser =
userBeingLoggedOut === activeUserId
? await firstValueFrom(this.accountService.nextUpAccount$.pipe(map((a) => a?.id)))
: null;
await this.stateEventRunnerService.handleEvent("logout", userId);
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.accountService.clean(userBeingLoggedOut);
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
await this.switchAccount(newActiveUser);
if (newActiveUser != null) {
// we have a new active user, do not continue tearing down application
await this.switchAccount(newActiveUser as UserId);
this.messagingService.send("switchAccountFinish");
} else {
this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId });
this.messagingService.send("doneLoggingOut", {
expired: expired,
userId: userBeingLoggedOut,
});
}
if (needStorageReseed) {

View File

@ -124,7 +124,7 @@
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
<value>Recognised as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
SECURE YOUR DIGITAL LIFE
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
@ -146,7 +146,7 @@ More reasons to choose Bitwarden:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private.
3rd-party Audits
3rd-Party Audits
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
Advanced 2FA
@ -159,13 +159,13 @@ Built-in Generator
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Bitwarden translations exist for more than 60 languages, translated by the global community through Crowdin.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, desktop OS, and more.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@ -133,7 +133,7 @@ ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
Utilise Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
EMPOWER YOUR TEAMS WITH BITWARDEN
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
@ -165,7 +165,7 @@ Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@ -118,10 +118,10 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden Password Manager</value>
<value>Bitwarden - Administrador de contraseñas</value>
</data>
<data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>En casa, en el trabajo o en el viaje, Bitwarden asegura fácilmente todas sus contraseñas, claves de acceso e información confidencial.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
@ -169,7 +169,7 @@ End-to-end encrypted credential management solutions from Bitwarden empower orga
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>En casa, en el trabajo o mientras viaja, Bitwarden protege fácilmente todas sus contraseñas, claves de acceso e información confidencial.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Sincroniza y accede a tu caja fuerte desde múltiples dispositivos</value>

View File

@ -165,7 +165,7 @@ Aplicações multiplataforma
Proteja e partilhe dados confidenciais dentro do seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel, ou SO de computador, e muito mais.
O Bitwarden protege mais do que apenas palavras-passe
As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Gestor de Segredos do Bitwarden e o Bitwarden Passwordless.dev!
As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Bitwarden - Gestor de Segredos e o Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">

View File

@ -8,7 +8,7 @@ import {
ViewContainerRef,
} from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, map, Subject, takeUntil } from "rxjs";
import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -567,7 +567,9 @@ export class AppComponent implements OnInit, OnDestroy {
this.messagingService.send("updateAppMenu", { updateRequest: updateRequest });
}
private async logOut(expired: boolean, userId?: string, reason?: LogoutReason) {
// Even though the userId parameter is no longer optional doesn't mean a message couldn't be
// passing null-ish values to us.
private async logOut(expired: boolean, userId: UserId, reason?: LogoutReason) {
if (reason) {
this.toastService.showToast({
variant: "error",
@ -578,18 +580,39 @@ export class AppComponent implements OnInit, OnDestroy {
await new Promise((resolve) => setTimeout(resolve, 5000));
}
const userBeingLoggedOut =
(userId as UserId) ??
(await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const userBeingLoggedOut = userId ?? activeUserId;
// Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted)
// doesn't attempt to update a user that is being logged out as we will manually
// call updateAppMenu when the logout is complete.
this.startAccountCleanUp(userBeingLoggedOut);
let preLogoutActiveUserId;
const nextUpAccount = await firstValueFrom(this.accountService.nextUpAccount$);
const nextUpAccount =
activeUserId === userBeingLoggedOut
? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts
: null;
try {
// HACK: We shouldn't wait for authentication status to change here but instead subscribe to the
// authentication status to do various actions.
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userBeingLoggedOut).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error(
"The logout process did not complete in a reasonable amount of time.",
);
},
}),
),
);
// Provide the userId of the user to upload events for
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
@ -603,26 +626,33 @@ export class AppComponent implements OnInit, OnDestroy {
await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut);
preLogoutActiveUserId = this.activeUserId;
await this.stateService.clean({ userId: userBeingLoggedOut });
await this.accountService.clean(userBeingLoggedOut);
// HACK: Wait for the user logging outs authentication status to transition to LoggedOut
await logoutPromise;
} finally {
this.finishAccountCleanUp(userBeingLoggedOut);
}
if (nextUpAccount == null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["login"]);
} else if (preLogoutActiveUserId !== nextUpAccount.id) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
// We only need to change the display at all if the account being looked at is the one
// being logged out. If it was a background account, no need to do anything.
if (userBeingLoggedOut === activeUserId) {
if (nextUpAccount != null) {
this.messagingService.send("switchAccount", { userId: nextUpAccount.id });
} else {
// We don't have another user to switch to, bring them to the login page so they
// can sign into a user.
await this.accountService.switchAccount(null);
void this.router.navigate(["login"]);
}
}
await this.updateAppMenu();
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
if (userBeingLoggedOut === preLogoutActiveUserId) {
if (userBeingLoggedOut === activeUserId) {
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
@ -713,7 +743,7 @@ export class AppComponent implements OnInit, OnDestroy {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
options[1] === "logOut"
? this.logOut(false, userId)
? this.logOut(false, userId as UserId)
: await this.vaultTimeoutService.lock(userId);
}
}

View File

@ -92,6 +92,11 @@ export class AccountSwitcherComponent {
return null;
}
if (!active.name && !active.email) {
// We need to have this information at minimum to display them.
return null;
}
return {
id: active.id,
name: active.name,

View File

@ -1549,11 +1549,11 @@
"message": "Set master password"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "Your organisation permissions were updated, requiring you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Your organisation requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
@ -1699,7 +1699,7 @@
"message": "An organisation policy is affecting your ownership options."
},
"personalOwnershipPolicyInEffectImports": {
"message": "An organization policy has blocked importing items into your individual vault."
"message": "An organisation policy has blocked importing items into your individual vault."
},
"allSends": {
"message": "All Sends",
@ -1895,7 +1895,7 @@
"message": "Your master password was recently changed by an administrator in your organisation. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
"message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tryAgain": {
"message": "Try again"
@ -1953,7 +1953,7 @@
}
},
"vaultTimeoutPolicyWithActionInEffect": {
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"placeholders": {
"hours": {
"content": "$1",
@ -1970,7 +1970,7 @@
}
},
"vaultTimeoutActionPolicyInEffect": {
"message": "Your organization policies have set your vault timeout action to $ACTION$.",
"message": "Your organisation policies have set your vault timeout action to $ACTION$.",
"placeholders": {
"action": {
"content": "$1",
@ -2060,7 +2060,7 @@
"message": "Exporting individual vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
@ -2346,7 +2346,7 @@
"message": "Region"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
"message": "Organisation SSO identifier is required."
},
"eu": {
"message": "EU",
@ -2532,7 +2532,7 @@
"message": "Total"
},
"importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?",
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?",
"placeholders": {
"organization": {
"content": "$1",

View File

@ -1549,11 +1549,11 @@
"message": "Set master password"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "Your organisation permissions were updated, requiring you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Your organisation requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
@ -1699,7 +1699,7 @@
"message": "An organization policy is affecting your ownership options."
},
"personalOwnershipPolicyInEffectImports": {
"message": "An organization policy has blocked importing items into your individual vault."
"message": "An organisation policy has blocked importing items into your individual vault."
},
"allSends": {
"message": "All Sends",
@ -1895,7 +1895,7 @@
"message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
"message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tryAgain": {
"message": "Try again"
@ -1953,7 +1953,7 @@
}
},
"vaultTimeoutPolicyWithActionInEffect": {
"message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.",
"placeholders": {
"hours": {
"content": "$1",
@ -1970,7 +1970,7 @@
}
},
"vaultTimeoutActionPolicyInEffect": {
"message": "Your organization policies have set your vault timeout action to $ACTION$.",
"message": "Your organisation policies have set your vault timeout action to $ACTION$.",
"placeholders": {
"action": {
"content": "$1",
@ -2060,7 +2060,7 @@
"message": "Exporting individual vault"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"placeholders": {
"email": {
"content": "$1",
@ -2346,7 +2346,7 @@
"message": "Region"
},
"ssoIdentifierRequired": {
"message": "Organization SSO identifier is required."
"message": "Organisation SSO identifier is required."
},
"eu": {
"message": "EU",
@ -2532,7 +2532,7 @@
"message": "Total"
},
"importWarning": {
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?",
"message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?",
"placeholders": {
"organization": {
"content": "$1",

View File

@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { concatMap, takeUntil, map } from "rxjs";
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
import { tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -8,12 +8,15 @@ import { ApiService } from "@bitwarden/common/abstractions/api.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 { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response";
import { AuthResponse } from "@bitwarden/common/auth/types/auth-response";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
@Component({
selector: "app-two-factor-setup",
@ -63,9 +66,18 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
async manage(type: TwoFactorProviderType) {
switch (type) {
case TwoFactorProviderType.OrganizationDuo: {
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
data: { type: type, organizationId: this.organizationId },
});
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
twoFactorVerifyDialogRef.closed,
);
if (!result) {
return;
}
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.type = TwoFactorProviderType.OrganizationDuo;
duoComp.organizationId = this.organizationId;
duoComp.auth(result);
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
});

View File

@ -2,7 +2,7 @@ import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery";
import { Subject, firstValueFrom, map, switchMap, takeUntil, timer } from "rxjs";
import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
@ -13,6 +13,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@ -136,9 +137,7 @@ export class AppComponent implements OnDestroy, OnInit {
this.router.navigate(["/"]);
break;
case "logout":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.logOut(!!message.expired, message.redirect);
await this.logOut(!!message.expired, message.redirect);
break;
case "lockVault":
await this.vaultTimeoutService.lock();
@ -266,7 +265,20 @@ export class AppComponent implements OnDestroy, OnInit {
private async logOut(expired: boolean, redirect = true) {
await this.eventUploadService.uploadEvents();
const userId = await this.stateService.getUserId();
const userId = (await this.stateService.getUserId()) as UserId;
const logoutPromise = firstValueFrom(
this.authService.authStatusFor$(userId).pipe(
filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut),
timeout({
first: 5_000,
with: () => {
throw new Error("The logout process did not complete in a reasonable amount of time.");
},
}),
),
);
await Promise.all([
this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(),
@ -274,11 +286,11 @@ export class AppComponent implements OnDestroy, OnInit {
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.passwordGenerationService.clear(),
this.biometricStateService.logout(userId as UserId),
this.biometricStateService.logout(userId),
this.paymentMethodWarningService.clear(),
]);
await this.stateEventRunnerService.handleEvent("logout", userId as UserId);
await this.stateEventRunnerService.handleEvent("logout", userId);
await this.searchService.clearIndex();
this.authService.logOut(async () => {
@ -291,6 +303,10 @@ export class AppComponent implements OnDestroy, OnInit {
}
await this.stateService.clean({ userId: userId });
await this.accountService.clean(userId);
await logoutPromise;
if (redirect) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "$ID$ silinməzdən əvvəl bütün müştəriləri (client) ayırmalısınız",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$",
"deleteProviderWarningDescription": {
"message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Heu de desenllaçar tots els clients abans de poder suprimir $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Před smazáním $ID$ musíte odpojit všechny klienty",
"deleteProviderWarningDescription": {
"message": "Před smazáním $ID$ musíte odpojit všechny klienty.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes",
"deleteProviderWarningDescription": {
"message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -6481,7 +6481,7 @@
"message": "Gewähre Zugriff auf Sammlungen, indem du diese zu dieser Gruppe hinzufügst."
},
"editGroupCollectionsRestrictionsDesc": {
"message": "You can only assign collections you manage."
"message": "Du kannst nur von dir verwaltete Sammlungen zuweisen."
},
"accessAllCollectionsDesc": {
"message": "Gewähre Zugriff auf alle aktuellen und zukünftigen Sammlungen."
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Du musst die Verknüpfung zu allen Kunden aufheben, bevor du $ID$ löschen kannst",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -579,7 +579,7 @@
"message": "Acceso"
},
"accessLevel": {
"message": "Access level"
"message": "Nivel de acceso"
},
"loggedOut": {
"message": "Sesión terminada"
@ -1359,11 +1359,11 @@
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)"
},
"onboardingImportDataDetailsPartTwoNoOrgs": {
"message": " instead.",
"message": " en su lugar.",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead."
},
"onboardingImportDataDetailsPartTwoWithOrgs": {
"message": " instead. You may need to wait until your administrator confirms your organization membership.",
"message": " en su lugar. Es posible que tenga que esperar hasta que su administrador confirme la pertenencia a su organización.",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership."
},
"importError": {
@ -1810,7 +1810,7 @@
"message": "Sitios web no seguros encontrados"
},
"unsecuredWebsitesFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
"message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con URIs no seguros. Deberías cambiar su esquema URI a https:// si el sitio web lo permite.",
"placeholders": {
"count": {
"content": "$1",
@ -1835,7 +1835,7 @@
"message": "Inicios de sesión sin 2FA encontrados"
},
"inactive2faFoundReportDesc": {
"message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
"message": "Hemos encontrado $COUNT$ sitio(s) web en su $VAULT$ que pueden no estar configurados con inicio de sesión en dos pasos (según 2fa.directory). Para proteger aún más estas cuentas, debe configurar el inicio de sesión en dos pasos.",
"placeholders": {
"count": {
"content": "$1",
@ -1863,7 +1863,7 @@
"message": "Contraseñas comprometidas encontradas"
},
"exposedPasswordsFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
"message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ que tienen contraseñas que fueron expuestas en violaciones de datos conocidas. Deberías cambiarlas para utilizar una contraseña nueva.",
"placeholders": {
"count": {
"content": "$1",
@ -1900,7 +1900,7 @@
"message": "Contraseñas débiles encontradas"
},
"weakPasswordsFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
"message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con contraseñas que no son seguras. Debe actualizarlos para utilizar contraseñas más seguras.",
"placeholders": {
"count": {
"content": "$1",
@ -1925,7 +1925,7 @@
"message": "Contraseñas reutilizadas encontradas"
},
"reusedPasswordsFoundReportDesc": {
"message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
"message": "Hemos encontrado $COUNT$ contraseñas que están siendo reutilizadas en tu $VAULT$. Deberías cambiarlas por un valor único.",
"placeholders": {
"count": {
"content": "$1",
@ -4971,13 +4971,13 @@
"message": "Cree una nueva organización de clientes que estará asociada a usted como proveedor. Usted poddrá acceder y gestionar esta organización."
},
"newClient": {
"message": "New client"
"message": "Nuevo cliente"
},
"addExistingOrganization": {
"message": "Añadir una organización existente"
},
"addNewOrganization": {
"message": "Add new organization"
"message": "Añadir nueva organización"
},
"myProvider": {
"message": "Mi proveedor"
@ -5233,7 +5233,7 @@
"message": "Set a unique SP entity ID"
},
"spUniqueEntityIdDesc": {
"message": "Generate an identifier that is unique to your organization"
"message": "Genera un identificador único para su organización"
},
"idpEntityId": {
"message": "ID de la entidad"
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -1810,7 +1810,7 @@
"message": "Suojaamattomia verkkosivustoja löytyi"
},
"unsecuredWebsitesFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.",
"message": "Löysimme holv(e)istasi $COUNT$ kohdetta suojaamattomilla URI-osoitteilla. Nämä tulisi muuttaa suojattuun \"https://\" -muotoon, jos sivustot sen mahdollistavat.",
"placeholders": {
"count": {
"content": "$1",
@ -1835,7 +1835,7 @@
"message": "Löytyi kirjautumistietoja, joille ei ole määritetty kaksivaiheista kirjautumista"
},
"inactive2faFoundReportDesc": {
"message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.",
"message": "Löysimme holv(e)istasi $COUNT$ sivustoa, joita ei ehkä ole määritetty käyttämään kaksivaiheista tunnistautumista (2fa.directory-sivuston mukaan). Nämä tilit tulisi suojata paremmin määrittämällä niille kaksivaiheinen tunnistautuminen.",
"placeholders": {
"count": {
"content": "$1",
@ -1863,7 +1863,7 @@
"message": "Paljastuneita salasanoja löytyi"
},
"exposedPasswordsFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
"message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat ovat paljastuneet tunnetuissa tietovuodoissa. Nämä salasanat tulisi vaihtaa.",
"placeholders": {
"count": {
"content": "$1",
@ -1900,7 +1900,7 @@
"message": "Heikkoja salasanoja löytyi"
},
"weakPasswordsFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.",
"message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat eivät ole vahvoja. Nämä tulisi korvata vahvemmilla salasanoilla.",
"placeholders": {
"count": {
"content": "$1",
@ -1925,7 +1925,7 @@
"message": "Toistuvia salasanoja löytyi"
},
"reusedPasswordsFoundReportDesc": {
"message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.",
"message": "Löysimme holv(e)istasi $COUNT$ toistuvasti käytettyä salasanaa. Ne tulisi korvata yksilöllisillä salasanoilla.",
"placeholders": {
"count": {
"content": "$1",
@ -6481,7 +6481,7 @@
"message": "Myönnä käyttöoikeudet kokoelmiin lisäämällä heidät tähän ryhmään."
},
"editGroupCollectionsRestrictionsDesc": {
"message": "You can only assign collections you manage."
"message": "Voit määrittää vain hallitsemiasi kokoelmia."
},
"accessAllCollectionsDesc": {
"message": "Myönnä käyttöoikeudet kaikkiin nykyisiin ja tuleviin kokoelmiin"
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Kaikki liitetyt asiakkaat on poistettava ennen toimittajan $ID$ poistoa.",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",
@ -8074,6 +8074,6 @@
"message": "Valitse kokoelman kohde"
},
"manageBillingFromProviderPortalMessage": {
"message": "Manage billing from the Provider Portal"
"message": "Hallitse laskutusta Toimittajaportaaliista"
}
}

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Vous devez dissocier tous les clients avant de pouvoir supprimer $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "$ID$ törlése előtt le kell választani az összes klienst.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -1863,7 +1863,7 @@
"message": "Password esposte trovate"
},
"exposedPasswordsFoundReportDesc": {
"message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.",
"message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte che hanno password che sono state esposte a violazioni di dati note. Dovresti cambiarli per usare una nuova password.",
"placeholders": {
"count": {
"content": "$1",
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Devi scollegare tutti i client prima di poter eliminare $ID$",
"deleteProviderWarningDescription": {
"message": "Devi scollegare tutti i client prima di poter eliminare $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "$ID$ を削除するに、すべてのクライアントのリンクを解除してください",
"deleteProviderWarningDescription": {
"message": "$ID$ を削除するまずすべてのクライアントのリンクを解除してください",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Ir jāatsaista visi klienti, pirms var izdzēst $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen",
"deleteProviderWarningDescription": {
"message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$",
"deleteProviderWarningDescription": {
"message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Você deve desvincular todos os clientes antes de excluir $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -1288,7 +1288,7 @@
"message": "Definições da chave de encriptação alteradas"
},
"dangerZone": {
"message": "Zona de perigo"
"message": "Zona de risco"
},
"dangerZoneDesc": {
"message": "Cuidado, estas ações são irreversíveis!"
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$",
"deleteProviderWarningDescription": {
"message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Перед удалением $ID$ необходимо отвязать всех клиентов",
"deleteProviderWarningDescription": {
"message": "Перед удалением $ID$ необходимо отвязать всех клиентов.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Pred tým než budete môcť odstrániť $ID$, musíte odpojiť všetkých klientov",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Морате прекинути везу са свим клијентима да бисте могли да избришете $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів",
"deleteProviderWarningDescription": {
"message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -579,7 +579,7 @@
"message": "Quyền truy cập"
},
"accessLevel": {
"message": "Access level"
"message": "Cấp độ truy cập"
},
"loggedOut": {
"message": "Đã đăng xuất"
@ -609,7 +609,7 @@
"message": "Đăng nhập bằng thiết bị"
},
"loginWithDeviceEnabledNote": {
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
"message": "Đăng nhập bằng thiết bị phải được thiết lập trong cài đặt của ứng dụng Bitwarden. Dùng cách khác?"
},
"loginWithMasterPassword": {
"message": "Đăng nhập bằng mật khẩu chính"
@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -7961,7 +7961,7 @@
}
}
},
"deleteProviderWarningDesc": {
"deleteProviderWarningDescription": {
"message": "删除 $ID$ 之前,您必须取消链接所有的客户端。",
"placeholders": {
"id": {
@ -7996,7 +7996,7 @@
"message": "集成"
},
"integrationsDesc": {
"message": "通过 Bitwarden 机密管理器将机密自动同步到第三方服务。"
"message": "自动将机密从 Bitwarden 机密管理器同步到第三方服务。"
},
"sdks": {
"message": "SDK"

View File

@ -7961,8 +7961,8 @@
}
}
},
"deleteProviderWarningDesc": {
"message": "You must unlink all clients before you can delete $ID$",
"deleteProviderWarningDescription": {
"message": "You must unlink all clients before you can delete $ID$.",
"placeholders": {
"id": {
"content": "$1",

View File

@ -81,8 +81,9 @@ export class AccountComponent {
if (providerClients.data != null && providerClients.data.length > 0) {
await this.dialogService.openSimpleDialog({
title: { key: "deleteProviderName", placeholders: [this.provider.name] },
content: { key: "deleteProviderWarningDesc", placeholders: [this.provider.name] },
content: { key: "deleteProviderWarningDescription", placeholders: [this.provider.name] },
acceptButtonText: { key: "ok" },
cancelButtonText: { key: "close" },
type: "danger",
});

View File

@ -244,7 +244,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts);
await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig);
await this.masterPasswordService.setMasterKey(masterKey, this.userId);
await this.cryptoService.setUserKey(userKey[0]);
await this.cryptoService.setUserKey(userKey[0], this.userId);
// Set private key only for new JIT provisioned users in MP encryption orgs
// Existing TDE users will have private key set on sync or on login
@ -253,7 +253,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
this.forceSetPasswordReason !=
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission
) {
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString);
await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, this.userId);
}
const localMasterKeyHash = await this.cryptoService.hashMasterKey(

View File

@ -140,7 +140,7 @@ describe("AuthRequestLoginStrategy", () => {
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled();
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
});
it("sets keys after a successful authentication when only userKey provided in login credentials", async () => {
@ -164,7 +164,7 @@ describe("AuthRequestLoginStrategy", () => {
// setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId);
// trustDeviceIfRequired should be called
expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled();

View File

@ -161,9 +161,13 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -252,7 +252,7 @@ export abstract class LoginStrategy {
await this.setMasterKey(response, userId);
await this.setUserKey(response, userId);
await this.setPrivateKey(response);
await this.setPrivateKey(response, userId);
this.messagingService.send("loggedIn");
@ -262,7 +262,7 @@ export abstract class LoginStrategy {
// The keys comes from different sources depending on the login strategy
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
// Old accounts used master key for encryption. We are forcing migrations but only need to
// check on password logins
@ -270,9 +270,10 @@ export abstract class LoginStrategy {
return false;
}
protected async createKeyPairForOldAccount() {
protected async createKeyPairForOldAccount(userId: UserId) {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
const userKey = await this.cryptoService.getUserKeyWithLegacySupport(userId);
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(userKey);
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {

View File

@ -178,7 +178,7 @@ describe("PasswordLoginStrategy", () => {
userId,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});
it("does not force the user to update their master password when there are no requirements", async () => {

View File

@ -233,9 +233,13 @@ export class PasswordLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -354,12 +354,16 @@ export class SsoLoginStrategy extends LoginStrategy {
await this.cryptoService.setUserKey(userKey);
}
protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
tokenResponse: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
const newSsoUser = tokenResponse.key == null;
if (!newSsoUser) {
await this.cryptoService.setPrivateKey(
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount()),
tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}
}

View File

@ -159,7 +159,7 @@ describe("UserApiLoginStrategy", () => {
await apiLogInStrategy.logIn(credentials);
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId);
});
it("gets and sets the master key if Key Connector is enabled", async () => {

View File

@ -116,9 +116,13 @@ export class UserApiLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -224,7 +224,7 @@ describe("WebAuthnLoginStrategy", () => {
mockPrfPrivateKey,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId);
// Master key and private key should not be set
expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled();

View File

@ -139,9 +139,13 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
}
}
protected override async setPrivateKey(response: IdentityTokenResponse): Promise<void> {
protected override async setPrivateKey(
response: IdentityTokenResponse,
userId: UserId,
): Promise<void> {
await this.cryptoService.setPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
response.privateKey ?? (await this.createKeyPairForOldAccount(userId)),
userId,
);
}

View File

@ -155,7 +155,7 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.cryptoService.setUserKey(userKey[0], userId);
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]);
try {
const keyConnectorUrl =

View File

@ -1,11 +1,12 @@
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { UserId } from "../../types/guid";
import { BaseResponse } from "./base.response";
export class ProfileResponse extends BaseResponse {
id: string;
id: UserId;
name: string;
email: string;
emailVerified: boolean;

View File

@ -54,13 +54,23 @@ export abstract class CryptoService {
* for encryption of data instead of the user key.
*/
abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise<boolean>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
*
* @deprecated Please provide the userId of the user you want the user key for.
*/
abstract getUserKeyWithLegacySupport(): Promise<UserKey>;
/**
* Use for encryption/decryption of data in order to support legacy
* encryption models. It will return the user key if available,
* if not it will return the master key.
* @param userId The desired user
*/
abstract getUserKeyWithLegacySupport(userId?: string): Promise<UserKey>;
abstract getUserKeyWithLegacySupport(userId: UserId): Promise<UserKey>;
/**
* Retrieves the user key from storage
* @param keySuffix The desired version of the user's key to retrieve
@ -169,10 +179,12 @@ export abstract class CryptoService {
* organization keys currently in memory
* @param orgs The organizations to set keys for
* @param providerOrgs The provider organizations to set keys for
* @param userId The user id of the user to set the org keys for
*/
abstract setOrgKeys(
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void>;
abstract activeUserOrgKeys$: Observable<Record<OrganizationId, OrgKey>>;
/**
@ -200,7 +212,13 @@ export abstract class CryptoService {
* @param providers The providers to set keys for
*/
abstract activeUserProviderKeys$: Observable<Record<ProviderId, ProviderKey>>;
abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise<void>;
/**
* Stores the provider keys for a given user.
* @param orgs The provider orgs for which to save the keys from.
* @param userId The user id of the user for which to store the keys for.
*/
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
/**
* @param providerId The desired provider
* @returns The provider's symmetric key
@ -228,7 +246,7 @@ export abstract class CryptoService {
* Note: does not clear the private key if null is provided
* @param encPrivateKey An encrypted private key
*/
abstract setPrivateKey(encPrivateKey: string): Promise<void>;
abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise<void>;
/**
* Returns the private key from memory. If not available, decrypts it
* from storage and stores it in memory
@ -247,8 +265,9 @@ export abstract class CryptoService {
* @param key A key to encrypt the private key with. If not provided,
* defaults to the user key
* @returns A new keypair: [publicKey in Base64, encrypted privateKey]
* @throws If the provided key is a null-ish value.
*/
abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>;
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* @param pin The user's pin
* @param salt The user's salt

View File

@ -395,12 +395,11 @@ export class CryptoService implements CryptoServiceAbstraction {
}
async setOrgKeys(
orgs: ProfileOrganizationResponse[] = [],
providerOrgs: ProfileProviderOrganizationResponse[] = [],
orgs: ProfileOrganizationResponse[],
providerOrgs: ProfileProviderOrganizationResponse[],
userId: UserId,
): Promise<void> {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.activeUserEncryptedOrgKeysState.update((_) => {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => {
const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {};
orgs.forEach((org) => {
@ -450,8 +449,8 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId);
}
async setProviderKeys(providers: ProfileProviderResponse[]): Promise<void> {
await this.activeUserEncryptedProviderKeysState.update((_) => {
async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void> {
await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => {
const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {};
providers.forEach((provider) => {
@ -494,12 +493,14 @@ export class CryptoService implements CryptoServiceAbstraction {
return [encShareKey, shareKey as T];
}
async setPrivateKey(encPrivateKey: EncryptedString): Promise<void> {
async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise<void> {
if (encPrivateKey == null) {
return;
}
await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey);
await this.stateProvider
.getUser(userId, USER_ENCRYPTED_PRIVATE_KEY)
.update(() => encPrivateKey);
}
async getPrivateKey(): Promise<Uint8Array> {
@ -523,9 +524,10 @@ export class CryptoService implements CryptoServiceAbstraction {
return this.hashPhrase(userFingerprint);
}
async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> {
// Default to user key
key ||= await this.getUserKeyWithLegacySupport();
async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> {
if (key == null) {
throw new Error("'key' is a required parameter and must be non-null.");
}
const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
const publicB64 = Utils.fromBufferToB64(keyPair[0]);

View File

@ -1,5 +1,5 @@
import { MockProxy, any, mock } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
import { BehaviorSubject, from, of } from "rxjs";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { SearchService } from "../../abstractions/search.service";
@ -106,6 +106,13 @@ describe("VaultTimeoutService", () => {
// Both are available by default and the specific test can change this per test
availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]);
authService.authStatusFor$.mockImplementation((userId) => {
return from([
accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut,
AuthenticationStatus.Locked,
]);
});
authService.getAuthStatus.mockImplementation((userId) => {
return Promise.resolve(accounts[userId]?.authStatus);
});
@ -387,18 +394,6 @@ describe("VaultTimeoutService", () => {
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1");
});
it("should call messaging service locked message if no user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock();
// Currently these pass `undefined` (or what they were given) as the userId back
// but we could change this to give the user that was locked (active) to these methods
// so they don't have to get it their own way, but that is a behavioral change that needs
// to be tested.
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined });
});
it("should call locked callback if no user passed into lock", async () => {
setupLock();
@ -414,25 +409,31 @@ describe("VaultTimeoutService", () => {
it("should call state event runner with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2");
await vaultTimeoutService.lock(user2);
expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", user2);
});
it("should call messaging service locked message with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" });
await vaultTimeoutService.lock(user2);
expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: user2 });
});
it("should call locked callback with user passed into lock", async () => {
setupLock();
await vaultTimeoutService.lock("user2");
const user2 = "user2" as UserId;
expect(lockedCallback).toHaveBeenCalledWith("user2");
await vaultTimeoutService.lock(user2);
expect(lockedCallback).toHaveBeenCalledWith(user2);
});
});
});

View File

@ -1,4 +1,4 @@
import { combineLatest, firstValueFrom, switchMap } from "rxjs";
import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs";
import { SearchService } from "../../abstractions/search.service";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
@ -80,7 +80,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
);
}
async lock(userId?: string): Promise<void> {
async lock(userId?: UserId): Promise<void> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
@ -94,7 +94,27 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.logOut(userId);
}
const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const currentUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const lockingUserId = userId ?? currentUserId;
// HACK: Start listening for the transition of the locking user from something to the locked state.
// This is very much a hack to ensure that the authentication status to retrievable right after
// it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead
// lockedCallback should be deprecated and people should subscribe and react to `authStatusFor$` themselves.
const lockPromise = firstValueFrom(
this.authService.authStatusFor$(lockingUserId).pipe(
filter((authStatus) => authStatus === AuthenticationStatus.Locked),
timeout({
first: 5_000,
with: () => {
throw new Error("The lock process did not complete in a reasonable amount of time.");
},
}),
),
);
if (userId == null || userId === currentUserId) {
await this.searchService.clearIndex();
@ -102,19 +122,21 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.collectionService.clearActiveUserCache();
}
await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId);
await this.masterPasswordService.clearMasterKey(lockingUserId);
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId });
await this.stateService.setCryptoMasterKeyAuto(null, { userId: lockingUserId });
await this.cipherService.clearCache(userId);
await this.cipherService.clearCache(lockingUserId);
await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId);
await this.stateEventRunnerService.handleEvent("lock", lockingUserId);
// FIXME: We should send the userId of the user that was locked, in the case of this method being passed
// undefined then it should give back the currentUserId. Better yet, this method shouldn't take
// an undefined userId at all. All receivers need to be checked for how they handle getting undefined.
this.messagingService.send("locked", { userId: userId });
// HACK: Sit here and wait for the the auth status to transition to `Locked`
// to ensure the message and lockedCallback will get the correct status
// if/when they call it.
await lockPromise;
this.messagingService.send("locked", { userId: lockingUserId });
if (this.lockedCallback != null) {
await this.lockedCallback(userId);
@ -162,7 +184,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
return diffSeconds >= vaultTimeoutSeconds;
}
private async executeTimeoutAction(userId: string): Promise<void> {
private async executeTimeoutAction(userId: UserId): Promise<void> {
const timeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId),
);

View File

@ -35,7 +35,6 @@ import { SendData } from "../../../tools/send/models/data/send.data";
import { SendResponse } from "../../../tools/send/models/response/send.response";
import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction";
import { InternalSendService } from "../../../tools/send/services/send.service.abstraction";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction";
import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
@ -311,7 +310,7 @@ export class SyncService implements SyncServiceAbstraction {
}
private async syncProfile(response: ProfileResponse) {
const stamp = await this.tokenService.getSecurityStamp(response.id as UserId);
const stamp = await this.tokenService.getSecurityStamp(response.id);
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
@ -321,15 +320,16 @@ export class SyncService implements SyncServiceAbstraction {
}
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
await this.cryptoService.setPrivateKey(response.privateKey);
await this.cryptoService.setProviderKeys(response.providers);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor);
await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId);
await this.accountService.setAccountEmailVerified(
response.id as UserId,
response.emailVerified,
await this.cryptoService.setPrivateKey(response.privateKey, response.id);
await this.cryptoService.setProviderKeys(response.providers, response.id);
await this.cryptoService.setOrgKeys(
response.organizations,
response.providerOrganizations,
response.id,
);
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
await this.billingAccountProfileStateService.setHasPremium(
response.premiumPersonally,

View File

@ -1,11 +1,11 @@
<div class="tw-flex tw-gap-2 tw-items-center">
<div class="tw-flex tw-gap-2 tw-items-center tw-w-full">
<ng-content select="[slot=start]"></ng-content>
<div class="tw-flex tw-flex-col tw-items-start tw-text-start tw-w-full [&_p]:tw-mb-0">
<div class="tw-text-main tw-text-base">
<div class="tw-text-main tw-text-base tw-w-full tw-truncate">
<ng-content></ng-content>
</div>
<div class="tw-text-muted tw-text-sm">
<div class="tw-text-muted tw-text-sm tw-w-full tw-truncate">
<ng-content select="[slot=secondary]"></ng-content>
</div>
</div>

View File

@ -7,7 +7,7 @@
: 'tw-border-b-secondary-300 [&:has(.item-main-content_button:hover,.item-main-content_a:hover)]:tw-border-b-transparent'
"
>
<bit-item-action class="item-main-content tw-block tw-w-full">
<bit-item-action class="item-main-content tw-block tw-flex-1 tw-overflow-hidden">
<ng-content></ng-content>
</bit-item-action>

View File

@ -113,11 +113,19 @@ export const TextOverflow: Story = {
render: (args) => ({
props: args,
template: /*html*/ `
<div class="tw-text-main tw-mb-4">TODO: Fix truncation</div>
<bit-item>
<bit-item-content>
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo!
<ng-container slot="secondary">Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd!</ng-container>
</bit-item-content>
<ng-container slot="end">
<bit-item-action>
<button type="button" bitIconButton="bwi-clone"></button>
</bit-item-action>
<bit-item-action>
<button type="button" bitIconButton="bwi-ellipsis-v"></button>
</bit-item-action>
</ng-container>
</bit-item>
`,
}),