Merge branch 'main' into ps/pm-7486/detect-libsecret-service

This commit is contained in:
Todd Martin 2024-04-23 11:44:33 -04:00 committed by GitHub
commit a0bffc21e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
154 changed files with 1421 additions and 1324 deletions

View File

@ -5,5 +5,6 @@
"**/locales/*[^n]/messages.json": true,
"**/_locales/[^e]*/messages.json": true,
"**/_locales/*[^n]/messages.json": true
}
},
"rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"]
}

View File

@ -1,6 +1,5 @@
{
"devFlags": {
"storeSessionDecrypted": false,
"managedEnvironment": {
"base": "https://localhost:8080"
}

View File

@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2024.4.1",
"version": "2024.4.2",
"scripts": {
"build": "webpack",
"build:mv3": "cross-env MANIFEST_VERSION=3 webpack",

View File

@ -23,7 +23,7 @@
"message": "Enterprise single sign-on"
},
"cancel": {
"message": "Cancel"
"message": "Canslo"
},
"close": {
"message": "Cau"
@ -318,7 +318,7 @@
"message": "Golygu"
},
"view": {
"message": "View"
"message": "Gweld"
},
"noItemsInList": {
"message": "Does dim eitemau i'w rhestru."
@ -549,10 +549,10 @@
"message": "Ydych chi'n siŵr eich bod am allgofnodi?"
},
"yes": {
"message": "Yes"
"message": "Ydw"
},
"no": {
"message": "No"
"message": "Na"
},
"unexpectedError": {
"message": "An unexpected error has occurred."

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden Passwortmanager",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.",
"message": "Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.",
"description": "Extension description"
},
"loginOrCreateNewAccount": {
@ -173,10 +173,10 @@
"message": "Master-Passwort ändern"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Weiter zur Web-App?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern."
},
"fingerprintPhrase": {
"message": "Fingerabdruck-Phrase",
@ -3001,7 +3001,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Erfolg"
},
"removePasskey": {
"message": "Passkey entfernen"
@ -3010,21 +3010,21 @@
"message": "Passkey entfernt"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich."
},
"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": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Weise diese Einträge einer Sammlung aus der",
"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": "zu, um sie sichtbar zu machen.",
"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": "Administrator-Konsole"
},
"errorAssigningTargetCollection": {
"message": "Fehler beim Zuweisen der Ziel-Sammlung."

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden 비밀번호 관리자",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.",
"message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.",
"description": "Extension description"
},
"loginOrCreateNewAccount": {
@ -173,7 +173,7 @@
"message": "마스터 비밀번호 변경"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "웹 앱에서 계속하시겠용?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
@ -688,10 +688,10 @@
"message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts."
},
"enableUsePasskeys": {
"message": "Ask to save and use passkeys"
"message": "패스키를 저장 및 사용할지 묻기"
},
"usePasskeysDesc": {
"message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts."
"message": "보관함에 새 패스키를 저장하거나 로그인할지 물어봅니다. 모든 로그인된 계정에 적용됩니다."
},
"notificationChangeDesc": {
"message": "Bitwarden에 저장되어 있는 비밀번호를 이 비밀번호로 변경하시겠습니까?"
@ -2786,55 +2786,55 @@
"message": "Confirm file password"
},
"typePasskey": {
"message": "Passkey"
"message": "패스키"
},
"passkeyNotCopied": {
"message": "Passkey will not be copied"
"message": "패스키가 복사되지 않습니다"
},
"passkeyNotCopiedAlert": {
"message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?"
"message": "패스키는 복제된 아이템에 복사되지 않습니다. 계속 이 항목을 복제하시겠어요?"
},
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password."
"message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다."
},
"logInWithPasskey": {
"message": "Log in with passkey?"
"message": "패스키로 로그인하시겠어요?"
},
"passkeyAlreadyExists": {
"message": "A passkey already exists for this application."
"message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다."
},
"noPasskeysFoundForThisApplication": {
"message": "No passkeys found for this application."
"message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다."
},
"noMatchingPasskeyLogin": {
"message": "You do not have a matching login for this site."
"message": "사이트와 일치하는 로그인이 없습니다."
},
"confirm": {
"message": "Confirm"
},
"savePasskey": {
"message": "Save passkey"
"message": "패스키 저장"
},
"savePasskeyNewLogin": {
"message": "Save passkey as new login"
"message": "새 로그인으로 패스키 저장"
},
"choosePasskey": {
"message": "Choose a login to save this passkey to"
"message": "패스키를 저장할 로그인 선택하기"
},
"passkeyItem": {
"message": "Passkey Item"
"message": "패스키 항목"
},
"overwritePasskey": {
"message": "Overwrite passkey?"
"message": "비밀번호를 덮어쓰시겠어요?"
},
"overwritePasskeyAlert": {
"message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?"
"message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?"
},
"featureNotSupported": {
"message": "Feature not yet supported"
},
"yourPasskeyIsLocked": {
"message": "Authentication required to use passkey. Verify your identity to continue."
"message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요."
},
"multifactorAuthenticationCancelled": {
"message": "Multifactor authentication cancelled"
@ -3004,10 +3004,10 @@
"message": "Success"
},
"removePasskey": {
"message": "Remove passkey"
"message": "패스키 제거"
},
"passkeyRemoved": {
"message": "Passkey removed"
"message": "패스키 제거됨"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."

View File

@ -531,7 +531,7 @@
"message": "Kan de QR-code van de huidige webpagina niet scannen"
},
"totpCaptureSuccess": {
"message": "Authenticatie-sleutel toegevoegd"
"message": "Authenticatiesleutel toegevoegd"
},
"totpCapture": {
"message": "Scan de authenticatie-QR-code van de huidige webpagina"
@ -1673,10 +1673,10 @@
"message": "Browserintegratie is niet ingeschakeld in de Bitwarden-desktopapplicatie. Schakel deze optie in de instellingen binnen de desktop-applicatie in."
},
"startDesktopTitle": {
"message": "Bitwarden-desktopapplicatie opstarten"
"message": "Bitwarden desktopapplicatie opstarten"
},
"startDesktopDesc": {
"message": "Je moet de Bitwarden-desktopapplicatie starten om deze functie te gebruiken."
"message": "Je moet de Bitwarden desktopapplicatie starten om deze functie te gebruiken."
},
"errorEnableBiometricTitle": {
"message": "Kon biometrie niet inschakelen"

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Menedżer Haseł Bitwarden",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.",
"message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.",
"description": "Extension description"
},
"loginOrCreateNewAccount": {

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden Gerenciador de Senhas",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.",
"message": "Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.",
"description": "Extension description"
},
"loginOrCreateNewAccount": {
@ -173,10 +173,10 @@
"message": "Alterar Senha Mestra"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Continuar no aplicativo web?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden."
},
"fingerprintPhrase": {
"message": "Frase Biométrica",
@ -500,10 +500,10 @@
"message": "A sua nova conta foi criada! Agora você pode iniciar a sessão."
},
"youSuccessfullyLoggedIn": {
"message": "You successfully logged in"
"message": "Você logou na sua conta com sucesso"
},
"youMayCloseThisWindow": {
"message": "You may close this window"
"message": "Você pode fechar esta janela"
},
"masterPassSent": {
"message": "Enviamos um e-mail com a dica da sua senha mestra."
@ -1500,7 +1500,7 @@
"message": "Código PIN inválido."
},
"tooManyInvalidPinEntryAttemptsLoggingOut": {
"message": "Too many invalid PIN entry attempts. Logging out."
"message": "Muitas tentativas de entrada de PIN inválidas. Desconectando."
},
"unlockWithBiometrics": {
"message": "Desbloquear com a biometria"
@ -2005,7 +2005,7 @@
"message": "Selecionar pasta..."
},
"noFoldersFound": {
"message": "No folders found",
"message": "Nenhuma pasta encontrada",
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
@ -2017,7 +2017,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
"message": "Verification required",
"message": "Verificação necessária",
"description": "Default title for the user verification dialog."
},
"hours": {
@ -2652,40 +2652,40 @@
}
},
"tryAgain": {
"message": "Try again"
"message": "Tentar novamente"
},
"verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue."
"message": "Verificação necessária para esta ação. Defina um PIN para continuar."
},
"setPin": {
"message": "Set PIN"
"message": "Definir PIN"
},
"verifyWithBiometrics": {
"message": "Verify with biometrics"
"message": "Verificiar com biometria"
},
"awaitingConfirmation": {
"message": "Awaiting confirmation"
"message": "Aguardando confirmação"
},
"couldNotCompleteBiometrics": {
"message": "Could not complete biometrics."
"message": "Não foi possível completar a biometria."
},
"needADifferentMethod": {
"message": "Need a different method?"
"message": "Precisa de um método diferente?"
},
"useMasterPassword": {
"message": "Use master password"
"message": "Usar a senha mestra"
},
"usePin": {
"message": "Use PIN"
"message": "Usar PIN"
},
"useBiometrics": {
"message": "Use biometrics"
"message": "Usar biometria"
},
"enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email."
"message": "Digite o código de verificação que foi enviado para o seu e-mail."
},
"resendCode": {
"message": "Resend code"
"message": "Reenviar código"
},
"total": {
"message": "Total"
@ -2700,19 +2700,19 @@
}
},
"launchDuoAndFollowStepsToFinishLoggingIn": {
"message": "Launch Duo and follow the steps to finish logging in."
"message": "Inicie o Duo e siga os passos para finalizar o login."
},
"duoRequiredForAccount": {
"message": "Duo two-step login is required for your account."
"message": "A autenticação em duas etapas do Duo é necessária para sua conta."
},
"popoutTheExtensionToCompleteLogin": {
"message": "Popout the extension to complete login."
"message": "Abra a extensão para concluir o login."
},
"popoutExtension": {
"message": "Popout extension"
"message": "Extensão pop-out"
},
"launchDuo": {
"message": "Launch Duo"
"message": "Abrir o Duo"
},
"importFormatError": {
"message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente."
@ -2846,13 +2846,13 @@
"message": "Nome de usuário ou senha incorretos"
},
"incorrectPassword": {
"message": "Incorrect password"
"message": "Senha incorreta"
},
"incorrectCode": {
"message": "Incorrect code"
"message": "Código incorreto"
},
"incorrectPin": {
"message": "Incorrect PIN"
"message": "PIN incorreto"
},
"multifactorAuthenticationFailed": {
"message": "Falha na autenticação de múltiplos fatores"
@ -2965,71 +2965,71 @@
"description": "Label indicating the most common import formats"
},
"overrideDefaultBrowserAutofillTitle": {
"message": "Make Bitwarden your default password manager?",
"message": "Tornar o Bitwarden seu gerenciador de senhas padrão?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillDescription": {
"message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.",
"message": "Ignorar esta opção pode causar conflitos entre o menu de autopreenchimento do Bitwarden e o do seu navegador.",
"description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutoFillSettings": {
"message": "Make Bitwarden your default password manager",
"message": "Faça do Bitwarden seu gerenciador de senhas padrão",
"description": "Label for the setting that allows overriding the default browser autofill settings"
},
"privacyPermissionAdditionNotGrantedTitle": {
"message": "Unable to set Bitwarden as the default password manager",
"message": "Não é possível definir o Bitwarden como o gerenciador de senhas padrão",
"description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"privacyPermissionAdditionNotGrantedDescription": {
"message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.",
"message": "Você deve conceder permissões de privacidade do navegador ao Bitwarden para defini-lo como o Gerenciador de Senhas padrão.",
"description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings"
},
"makeDefault": {
"message": "Make default",
"message": "Tornar padrão",
"description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
"message": "Credentials saved successfully!",
"message": "Credenciais salvas com sucesso!",
"description": "Notification message for when saving credentials has succeeded."
},
"updateCipherAttemptSuccess": {
"message": "Credentials updated successfully!",
"message": "Credenciais atualizadas com sucesso!",
"description": "Notification message for when updating credentials has succeeded."
},
"saveCipherAttemptFailed": {
"message": "Error saving credentials. Check console for details.",
"message": "Erro ao salvar credenciais. Verifique o console para detalhes.",
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Sucesso"
},
"removePasskey": {
"message": "Remove passkey"
"message": "Remover senha"
},
"passkeyRemoved": {
"message": "Passkey removed"
"message": "Chave de acesso removida"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração."
},
"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": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"message": "Atribua estes itens a uma coleção da",
"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 torná-los visíveis.",
"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": "Painel de administração"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Erro ao atribuir coleção de destino."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Erro ao atribuir pasta de destino."
}
}

View File

@ -173,10 +173,10 @@
"message": "Промени главну лозинку"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Ићи на веб апликацију?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Можете променити главну лозинку на Bitwarden веб апликацији."
},
"fingerprintPhrase": {
"message": "Сигурносна Фраза Сефа",
@ -3001,7 +3001,7 @@
"description": "Notification message for when saving credentials has failed."
},
"success": {
"message": "Success"
"message": "Успех"
},
"removePasskey": {
"message": "Уклонити приступачни кључ"
@ -3010,10 +3010,10 @@
"message": "Приступачни кључ је уклоњен"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле."
},
"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": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле."
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
@ -3024,12 +3024,12 @@
"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": "Администраторска конзола"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Грешка при додељивању циљне колекције."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Грешка при додељивању циљне фасцикле."
}
}

View File

@ -3,11 +3,11 @@
"message": "Bitwarden"
},
"extName": {
"message": "Bitwarden Password Manager",
"message": "Bitwarden 密码管理器",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
"message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.",
"message": "无论是在家里、工作中还是在外出时Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。",
"description": "Extension description"
},
"loginOrCreateNewAccount": {
@ -176,7 +176,7 @@
"message": "前往网页 App 吗?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
},
"fingerprintPhrase": {
"message": "指纹短语",
@ -3010,17 +3010,17 @@
"message": "通行密钥已移除"
},
"unassignedItemsBannerNotice": {
"message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console."
"message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。"
},
"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": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。"
},
"unassignedItemsBannerCTAPartOne": {
"message": "Assign these items to a collection from the",
"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": "以使其可见。",
"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": {

View File

@ -98,7 +98,9 @@ import { StateFactory } from "@bitwarden/common/platform/factories/state-factory
import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging";
// eslint-disable-next-line no-restricted-imports -- Used for dependency creation
import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service";
import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service";
@ -111,7 +113,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { UserKeyInitService } from "@bitwarden/common/platform/services/user-key-init.service";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
@ -226,6 +227,7 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service";
import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging";
import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service";
@ -246,6 +248,8 @@ export default class MainBackground {
secureStorageService: AbstractStorageService;
memoryStorageService: AbstractMemoryStorageService;
memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService;
largeObjectMemoryStorageForStateProviders: AbstractMemoryStorageService &
ObservableStorageService;
i18nService: I18nServiceAbstraction;
platformUtilsService: PlatformUtilsServiceAbstraction;
logService: LogServiceAbstraction;
@ -402,34 +406,57 @@ export default class MainBackground {
self,
);
const mv3MemoryStorageCreator = (partitionName: string) => {
if (this.popupOnlyContext) {
return new ForegroundMemoryStorageService(partitionName);
// Creates a session key for mv3 storage of large memory items
const sessionKey = new Lazy(async () => {
// Key already in session storage
const sessionStorage = new BrowserMemoryStorageService();
const existingKey = await sessionStorage.get<SymmetricCryptoKey>("session-key");
if (existingKey) {
if (sessionStorage.valuesRequireDeserialization) {
return SymmetricCryptoKey.fromJSON(existingKey);
}
return existingKey;
}
// New key
const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
await sessionStorage.save("session-key", derivedKey);
return derivedKey;
});
const mv3MemoryStorageCreator = () => {
if (this.popupOnlyContext) {
return new ForegroundMemoryStorageService();
}
// TODO: Consider using multithreaded encrypt service in popup only context
return new LocalBackedSessionStorageService(
this.logService,
sessionKey,
this.storageService,
new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false),
this.keyGenerationService,
new BrowserLocalStorageService(),
new BrowserMemoryStorageService(),
this.platformUtilsService,
partitionName,
this.logService,
);
};
this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used
this.memoryStorageService = BrowserApi.isManifestVersion(3)
? mv3MemoryStorageCreator("stateService")
: new MemoryStorageService();
this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
? mv3MemoryStorageCreator("stateProviders")
: new BackgroundMemoryStorageService();
? new BrowserMemoryStorageService() // mv3 stores to storage.session
: new BackgroundMemoryStorageService(); // mv2 stores to memory
this.memoryStorageService = BrowserApi.isManifestVersion(3)
? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3
: new MemoryStorageService();
this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3)
? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage
: this.memoryStorageForStateProviders; // mv2 stores to the same location
const storageServiceProvider = new StorageServiceProvider(
const storageServiceProvider = new BrowserStorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
this.largeObjectMemoryStorageForStateProviders,
);
this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider);
@ -466,9 +493,7 @@ export default class MainBackground {
this.accountService,
this.singleUserStateProvider,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(
this.memoryStorageForStateProviders,
);
this.derivedStateProvider = new BackgroundDerivedStateProvider(storageServiceProvider);
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
this.singleUserStateProvider,
@ -788,7 +813,6 @@ export default class MainBackground {
this.avatarService,
logoutCallback,
this.billingAccountProfileStateService,
this.tokenService,
);
this.eventUploadService = new EventUploadService(
this.apiService,
@ -1081,20 +1105,22 @@ export default class MainBackground {
await (this.eventUploadService as EventUploadService).init(true);
this.twoFactorService.init();
if (!this.popupOnlyContext) {
await this.vaultTimeoutService.init(true);
this.fido2Background.init();
await this.runtimeBackground.init();
await this.notificationBackground.init();
this.filelessImporterBackground.init();
await this.commandsBackground.init();
await this.overlayBackground.init();
await this.tabsBackground.init();
this.contextMenusBackground?.init();
await this.idleBackground.init();
if (BrowserApi.isManifestVersion(2)) {
await this.webRequestBackground.init();
}
if (this.popupOnlyContext) {
return;
}
await this.vaultTimeoutService.init(true);
this.fido2Background.init();
await this.runtimeBackground.init();
await this.notificationBackground.init();
this.filelessImporterBackground.init();
await this.commandsBackground.init();
await this.overlayBackground.init();
await this.tabsBackground.init();
this.contextMenusBackground?.init();
await this.idleBackground.init();
if (BrowserApi.isManifestVersion(2)) {
await this.webRequestBackground.init();
}
if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) {

View File

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2024.4.1",
"version": "2024.4.2",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
"version": "2024.4.1",
"version": "2024.4.2",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
@ -59,7 +59,6 @@
"clipboardRead",
"clipboardWrite",
"idle",
"alarms",
"scripting",
"offscreen"
],

View File

@ -4,14 +4,14 @@ import { BackgroundDerivedStateProvider } from "../../state/background-derived-s
import { CachedServices, FactoryOptions, factory } from "./factory-options";
import {
MemoryStorageServiceInitOptions,
observableMemoryStorageServiceFactory,
} from "./storage-service.factory";
StorageServiceProviderInitOptions,
storageServiceProviderFactory,
} from "./storage-service-provider.factory";
type DerivedStateProviderFactoryOptions = FactoryOptions;
export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions &
MemoryStorageServiceInitOptions;
StorageServiceProviderInitOptions;
export async function derivedStateProviderFactory(
cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices,
@ -22,6 +22,6 @@ export async function derivedStateProviderFactory(
"derivedStateProvider",
opts,
async () =>
new BackgroundDerivedStateProvider(await observableMemoryStorageServiceFactory(cache, opts)),
new BackgroundDerivedStateProvider(await storageServiceProviderFactory(cache, opts)),
);
}

View File

@ -3,6 +3,8 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { BrowserApi } from "../../browser/browser-api";
@ -17,10 +19,10 @@ import {
KeyGenerationServiceInitOptions,
keyGenerationServiceFactory,
} from "./key-generation-service.factory";
import { logServiceFactory, LogServiceInitOptions } from "./log-service.factory";
import { LogServiceInitOptions, logServiceFactory } from "./log-service.factory";
import {
platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions,
platformUtilsServiceFactory,
} from "./platform-utils-service.factory";
export type DiskStorageServiceInitOptions = FactoryOptions;
@ -70,13 +72,23 @@ export function memoryStorageServiceFactory(
return factory(cache, "memoryStorageService", opts, async () => {
if (BrowserApi.isManifestVersion(3)) {
return new LocalBackedSessionStorageService(
await logServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
new Lazy(async () => {
const existingKey = await (
await sessionStorageServiceFactory(cache, opts)
).get<SymmetricCryptoKey>("session-key");
if (existingKey) {
return existingKey;
}
const { derivedKey } = await (
await keyGenerationServiceFactory(cache, opts)
).createKeyWithPurpose(128, "ephemeral", "bitwarden-ephemeral");
await (await sessionStorageServiceFactory(cache, opts)).save("session-key", derivedKey);
return derivedKey;
}),
await diskStorageServiceFactory(cache, opts),
await sessionStorageServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),
"serviceFactories",
await logServiceFactory(cache, opts),
);
}
return new MemoryStorageService();

View File

@ -9,7 +9,7 @@ jest.mock("../flags", () => ({
}));
class TestClass {
@devFlag("storeSessionDecrypted") test() {
@devFlag("managedEnvironment") test() {
return "test";
}
}

View File

@ -19,7 +19,6 @@ export type Flags = {
// required to avoid linting errors when there are no flags
// eslint-disable-next-line @typescript-eslint/ban-types
export type DevFlags = {
storeSessionDecrypted?: boolean;
managedEnvironment?: GroupPolicyEnvironment;
} & SharedDevFlags;

View File

@ -1,7 +1,16 @@
import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
import AbstractChromeStorageService from "./abstractions/abstract-chrome-storage-api.service";
export default class BrowserMemoryStorageService extends AbstractChromeStorageService {
export default class BrowserMemoryStorageService
extends AbstractChromeStorageService
implements AbstractMemoryStorageService
{
constructor() {
super(chrome.storage.session);
}
type = "MemoryStorageService" as const;
getBypassCache<T>(key: string): Promise<T> {
return this.get(key);
}
}

View File

@ -1,412 +1,200 @@
import { mock, MockProxy } from "jest-mock-extended";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
AbstractMemoryStorageService,
AbstractStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BrowserApi } from "../browser/browser-api";
import { FakeStorageService, makeEncString } from "@bitwarden/common/spec";
import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service";
describe.skip("LocalBackedSessionStorage", () => {
const sendMessageWithResponseSpy: jest.SpyInstance = jest.spyOn(
BrowserApi,
"sendMessageWithResponse",
describe("LocalBackedSessionStorage", () => {
const sessionKey = new SymmetricCryptoKey(
Utils.fromUtf8ToArray("00000000000000000000000000000000"),
);
let localStorage: FakeStorageService;
let encryptService: MockProxy<EncryptService>;
let keyGenerationService: MockProxy<KeyGenerationService>;
let localStorageService: MockProxy<AbstractStorageService>;
let sessionStorageService: MockProxy<AbstractMemoryStorageService>;
let logService: MockProxy<LogService>;
let platformUtilsService: MockProxy<PlatformUtilsService>;
let cache: Record<string, unknown>;
const testObj = { a: 1, b: 2 };
const stringifiedTestObj = JSON.stringify(testObj);
const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000"));
let getSessionKeySpy: jest.SpyInstance;
let sendUpdateSpy: jest.SpyInstance<void, [storageUpdate: StorageUpdate]>;
const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input));
let logService: MockProxy<LogService>;
let sut: LocalBackedSessionStorageService;
const mockExistingSessionKey = (key: SymmetricCryptoKey) => {
sessionStorageService.get.mockImplementation((storageKey) => {
if (storageKey === "localEncryptionKey_test") {
return Promise.resolve(key?.toJSON());
}
return Promise.reject("No implementation for " + storageKey);
});
};
beforeEach(() => {
sendMessageWithResponseSpy.mockResolvedValue(null);
logService = mock<LogService>();
localStorage = new FakeStorageService();
encryptService = mock<EncryptService>();
keyGenerationService = mock<KeyGenerationService>();
localStorageService = mock<AbstractStorageService>();
sessionStorageService = mock<AbstractMemoryStorageService>();
platformUtilsService = mock<PlatformUtilsService>();
logService = mock<LogService>();
sut = new LocalBackedSessionStorageService(
logService,
new Lazy(async () => sessionKey),
localStorage,
encryptService,
keyGenerationService,
localStorageService,
sessionStorageService,
platformUtilsService,
"test",
logService,
);
cache = sut["cachedSession"];
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
derivedKey: key,
salt: "bitwarden-ephemeral",
material: null, // Not used
});
getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey");
getSessionKeySpy.mockResolvedValue(key);
// sendUpdateSpy = jest.spyOn(sut, "sendUpdate");
// sendUpdateSpy.mockReturnValue();
});
describe("get", () => {
describe("in local cache or external context cache", () => {
it("should return from local cache", async () => {
cache["test"] = stringifiedTestObj;
const result = await sut.get("test");
expect(result).toStrictEqual(testObj);
});
it("should return from external context cache when local cache is not available", async () => {
sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj);
const result = await sut.get("test");
expect(result).toStrictEqual(testObj);
});
it("return the cached value when one is cached", async () => {
sut["cache"]["test"] = "cached";
const result = await sut.get("test");
expect(result).toEqual("cached");
});
describe("not in cache", () => {
const session = { test: stringifiedTestObj };
it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.get("test");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
expect(result).toEqual("decrypted");
});
beforeEach(() => {
mockExistingSessionKey(key);
});
it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
await sut.get("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
});
});
describe("no session retrieved", () => {
let result: any;
let spy: jest.SpyInstance;
beforeEach(async () => {
spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
localStorageService.get.mockResolvedValue(null);
result = await sut.get("test");
});
describe("getBypassCache", () => {
it("ignores cached values", async () => {
sut["cache"]["test"] = "cached";
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.getBypassCache("test");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
expect(result).toEqual("decrypted");
});
it("should grab from session if not in cache", async () => {
expect(spy).toHaveBeenCalledWith(key);
});
it("returns a decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.getBypassCache("test");
expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey);
expect(result).toEqual("decrypted");
});
it("should return null if session is null", async () => {
expect(result).toBeNull();
});
});
it("caches the decrypted value when one is stored in local storage", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
await sut.getBypassCache("test");
expect(sut["cache"]["test"]).toEqual("decrypted");
});
describe("session retrieved from storage", () => {
beforeEach(() => {
jest.spyOn(sut, "getLocalSession").mockResolvedValue(session);
});
it("should return null if session does not have the key", async () => {
const result = await sut.get("DNE");
expect(result).toBeNull();
});
it("should return the value retrieved from session", async () => {
const result = await sut.get("test");
expect(result).toEqual(session.test);
});
it("should set retrieved values in cache", async () => {
await sut.get("test");
expect(cache["test"]).toBeTruthy();
expect(cache["test"]).toEqual(session.test);
});
it("should use a deserializer if provided", async () => {
const deserializer = jest.fn().mockReturnValue(testObj);
const result = await sut.get("test", { deserializer: deserializer });
expect(deserializer).toHaveBeenCalledWith(session.test);
expect(result).toEqual(testObj);
});
});
it("deserializes when a deserializer is provided", async () => {
const encrypted = makeEncString("encrypted");
localStorage.internalStore["session_test"] = encrypted.encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const deserializer = jest.fn().mockReturnValue("deserialized");
const result = await sut.getBypassCache("test", { deserializer });
expect(deserializer).toHaveBeenCalledWith("decrypted");
expect(result).toEqual("deserialized");
});
});
describe("has", () => {
it("should be false if `get` returns null", async () => {
const spy = jest.spyOn(sut, "get");
spy.mockResolvedValue(null);
expect(await sut.has("test")).toBe(false);
it("returns false when the key is not in cache", async () => {
const result = await sut.has("test");
expect(result).toBe(false);
});
it("returns true when the key is in cache", async () => {
sut["cache"]["test"] = "cached";
const result = await sut.has("test");
expect(result).toBe(true);
});
it("returns true when the key is in local storage", async () => {
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted"));
const result = await sut.has("test");
expect(result).toBe(true);
});
it.each([null, undefined])("returns false when %s is cached", async (nullish) => {
sut["cache"]["test"] = nullish;
await expect(sut.has("test")).resolves.toBe(false);
});
it.each([null, undefined])(
"returns false when null is stored in local storage",
async (nullish) => {
localStorage.internalStore["session_test"] = nullish;
await expect(sut.has("test")).resolves.toBe(false);
expect(encryptService.decryptToUtf8).not.toHaveBeenCalled();
},
);
});
describe("save", () => {
const encString = makeEncString("encrypted");
beforeEach(() => {
encryptService.encrypt.mockResolvedValue(encString);
});
it("logs a warning when saving the same value twice and in a dev environment", async () => {
platformUtilsService.isDev.mockReturnValue(true);
sut["cache"]["test"] = "cached";
await sut.save("test", "cached");
expect(logService.warning).toHaveBeenCalled();
});
it("does not log when saving the same value twice and not in a dev environment", async () => {
platformUtilsService.isDev.mockReturnValue(false);
sut["cache"]["test"] = "cached";
await sut.save("test", "cached");
expect(logService.warning).not.toHaveBeenCalled();
});
it("removes the key when saving a null value", async () => {
const spy = jest.spyOn(sut, "remove");
await sut.save("test", null);
expect(spy).toHaveBeenCalledWith("test");
});
it("should be true if `get` returns non-null", async () => {
const spy = jest.spyOn(sut, "get");
spy.mockResolvedValue({});
expect(await sut.has("test")).toBe(true);
expect(spy).toHaveBeenCalledWith("test");
it("saves the value to cache", async () => {
await sut.save("test", "value");
expect(sut["cache"]["test"]).toEqual("value");
});
it("encrypts and saves the value to local storage", async () => {
await sut.save("test", "value");
expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey);
expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString);
});
it("emits an update", async () => {
const spy = jest.spyOn(sut["updatesSubject"], "next");
await sut.save("test", "value");
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
});
});
describe("remove", () => {
describe("existing cache value is null", () => {
it("should not save null if the local cached value is already null", async () => {
cache["test"] = null;
await sut.remove("test");
expect(sendUpdateSpy).not.toHaveBeenCalled();
});
it("should not save null if the externally cached value is already null", async () => {
sendMessageWithResponseSpy.mockResolvedValue(null);
await sut.remove("test");
expect(sendUpdateSpy).not.toHaveBeenCalled();
});
});
it("should save null", async () => {
cache["test"] = stringifiedTestObj;
it("nulls the value in cache", async () => {
sut["cache"]["test"] = "cached";
await sut.remove("test");
expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
});
});
describe("save", () => {
describe("currently cached", () => {
it("does not save the value a local cached value exists which is an exact match", async () => {
cache["test"] = stringifiedTestObj;
await sut.save("test", testObj);
expect(sendUpdateSpy).not.toHaveBeenCalled();
});
it("does not save the value if a local cached value exists, even if the keys not in the same order", async () => {
cache["test"] = JSON.stringify({ b: 2, a: 1 });
await sut.save("test", testObj);
expect(sendUpdateSpy).not.toHaveBeenCalled();
});
it("does not save the value a externally cached value exists which is an exact match", async () => {
sendMessageWithResponseSpy.mockResolvedValue(stringifiedTestObj);
await sut.save("test", testObj);
expect(sendUpdateSpy).not.toHaveBeenCalled();
expect(cache["test"]).toBe(stringifiedTestObj);
});
it("saves the value if the currently cached string value evaluates to a falsy value", async () => {
cache["test"] = "null";
await sut.save("test", testObj);
expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "save" });
});
expect(sut["cache"]["test"]).toBeNull();
});
describe("caching", () => {
beforeEach(() => {
localStorageService.get.mockResolvedValue(null);
sessionStorageService.get.mockResolvedValue(null);
localStorageService.save.mockResolvedValue();
sessionStorageService.save.mockResolvedValue();
encryptService.encrypt.mockResolvedValue(mockEnc("{}"));
});
it("should remove key from cache if value is null", async () => {
cache["test"] = {};
// const cacheSetSpy = jest.spyOn(cache, "set");
expect(cache["test"]).toBe(true);
await sut.save("test", null);
// Don't remove from cache, just replace with null
expect(cache["test"]).toBe(null);
// expect(cacheSetSpy).toHaveBeenCalledWith("test", null);
});
it("should set cache if value is non-null", async () => {
expect(cache["test"]).toBe(false);
// const setSpy = jest.spyOn(cache, "set");
await sut.save("test", testObj);
expect(cache["test"]).toBe(stringifiedTestObj);
// expect(setSpy).toHaveBeenCalledWith("test", stringifiedTestObj);
});
it("removes the key from local storage", async () => {
localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString;
await sut.remove("test");
expect(localStorage.internalStore["session_test"]).toBeUndefined();
});
describe("local storing", () => {
let setSpy: jest.SpyInstance;
beforeEach(() => {
setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue();
});
it("should store a new session", async () => {
jest.spyOn(sut, "getLocalSession").mockResolvedValue(null);
await sut.save("test", testObj);
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
});
it("should update an existing session", async () => {
const existingObj = { test: testObj };
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
await sut.save("test2", testObj);
expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key);
});
it("should overwrite an existing item in session", async () => {
const existingObj = { test: {} };
jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj);
await sut.save("test", testObj);
expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key);
});
});
});
describe("getSessionKey", () => {
beforeEach(() => {
getSessionKeySpy.mockRestore();
});
it("should return the stored symmetric crypto key", async () => {
sessionStorageService.get.mockResolvedValue({ ...key });
const result = await sut.getSessionEncKey();
expect(result).toStrictEqual(key);
});
describe("new key creation", () => {
beforeEach(() => {
keyGenerationService.createKeyWithPurpose.mockResolvedValue({
salt: "salt",
material: null,
derivedKey: key,
});
jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
});
it("should create a symmetric crypto key", async () => {
const result = await sut.getSessionEncKey();
expect(result).toStrictEqual(key);
expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1);
});
it("should store a symmetric crypto key if it makes one", async () => {
const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
await sut.getSessionEncKey();
expect(spy).toHaveBeenCalledWith(key);
});
});
});
describe("getLocalSession", () => {
it("should return null if session is null", async () => {
const result = await sut.getLocalSession(key);
expect(result).toBeNull();
expect(localStorageService.get).toHaveBeenCalledWith("session_test");
});
describe("non-null sessions", () => {
const session = { test: "test" };
const encSession = new EncString(JSON.stringify(session));
const decryptedSession = JSON.stringify(session);
beforeEach(() => {
localStorageService.get.mockResolvedValue(encSession.encryptedString);
});
it("should decrypt returned sessions", async () => {
encryptService.decryptToUtf8
.calledWith(expect.anything(), key)
.mockResolvedValue(decryptedSession);
await sut.getLocalSession(key);
expect(encryptService.decryptToUtf8).toHaveBeenNthCalledWith(1, encSession, key);
});
it("should parse session", async () => {
encryptService.decryptToUtf8
.calledWith(expect.anything(), key)
.mockResolvedValue(decryptedSession);
const result = await sut.getLocalSession(key);
expect(result).toEqual(session);
});
it("should remove state if decryption fails", async () => {
encryptService.decryptToUtf8.mockResolvedValue(null);
const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue();
const result = await sut.getLocalSession(key);
expect(result).toBeNull();
expect(setSessionEncKeySpy).toHaveBeenCalledWith(null);
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
});
});
});
describe("setLocalSession", () => {
const testSession = { test: "a" };
const testJSON = JSON.stringify(testSession);
it("should encrypt a stringified session", async () => {
encryptService.encrypt.mockImplementation(mockEnc);
localStorageService.save.mockResolvedValue();
await sut.setLocalSession(testSession, key);
expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key);
});
it("should remove local session if null", async () => {
encryptService.encrypt.mockResolvedValue(null);
await sut.setLocalSession(null, key);
expect(localStorageService.remove).toHaveBeenCalledWith("session_test");
});
it("should save encrypted string", async () => {
encryptService.encrypt.mockImplementation(mockEnc);
await sut.setLocalSession(testSession, key);
expect(localStorageService.save).toHaveBeenCalledWith(
"session_test",
(await mockEnc(testJSON)).encryptedString,
);
});
});
describe("setSessionKey", () => {
it("should remove if null", async () => {
await sut.setSessionEncKey(null);
expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test");
});
it("should save key when not null", async () => {
await sut.setSessionEncKey(key);
expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key);
it("emits an update", async () => {
const spy = jest.spyOn(sut["updatesSubject"], "next");
await sut.remove("test");
expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" });
});
});
});

View File

@ -2,7 +2,6 @@ import { Subject } from "rxjs";
import { Jsonify } from "type-fest";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
@ -11,13 +10,12 @@ import {
ObservableStorageService,
StorageUpdate,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { Lazy } from "@bitwarden/common/platform/misc/lazy";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { BrowserApi } from "../browser/browser-api";
import { devFlag } from "../decorators/dev-flag.decorator";
import { devFlagEnabled } from "../flags";
import { MemoryStoragePortMessage } from "../storage/port-messages";
import { portName } from "../storage/port-name";
@ -25,85 +23,64 @@ export class LocalBackedSessionStorageService
extends AbstractMemoryStorageService
implements ObservableStorageService
{
private ports: Set<chrome.runtime.Port> = new Set([]);
private cache: Record<string, unknown> = {};
private updatesSubject = new Subject<StorageUpdate>();
private commandName = `localBackedSessionStorage_${this.partitionName}`;
private encKey = `localEncryptionKey_${this.partitionName}`;
private sessionKey = `session_${this.partitionName}`;
private cachedSession: Record<string, unknown> = {};
private _ports: Set<chrome.runtime.Port> = new Set([]);
private knownNullishCacheKeys: Set<string> = new Set([]);
readonly valuesRequireDeserialization = true;
updates$ = this.updatesSubject.asObservable();
constructor(
private logService: LogService,
private encryptService: EncryptService,
private keyGenerationService: KeyGenerationService,
private localStorage: AbstractStorageService,
private sessionStorage: AbstractStorageService,
private platformUtilsService: PlatformUtilsService,
private partitionName: string,
private readonly sessionKey: Lazy<Promise<SymmetricCryptoKey>>,
private readonly localStorage: AbstractStorageService,
private readonly encryptService: EncryptService,
private readonly platformUtilsService: PlatformUtilsService,
private readonly logService: LogService,
) {
super();
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {
if (port.name !== `${portName(chrome.storage.session)}_${partitionName}`) {
if (port.name !== portName(chrome.storage.session)) {
return;
}
this._ports.add(port);
this.ports.add(port);
const listenerCallback = this.onMessageFromForeground.bind(this);
port.onDisconnect.addListener(() => {
this._ports.delete(port);
this.ports.delete(port);
port.onMessage.removeListener(listenerCallback);
});
port.onMessage.addListener(listenerCallback);
// Initialize the new memory storage service with existing data
this.sendMessageTo(port, {
action: "initialization",
data: Array.from(Object.keys(this.cachedSession)),
data: Array.from(Object.keys(this.cache)),
});
this.updates$.subscribe((update) => {
this.broadcastMessage({
action: "subject_update",
data: update,
});
});
});
this.updates$.subscribe((update) => {
this.broadcastMessage({
action: "subject_update",
data: update,
});
});
}
get valuesRequireDeserialization(): boolean {
return true;
}
get updates$() {
return this.updatesSubject.asObservable();
}
async get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
if (this.cachedSession[key] != null) {
return this.cachedSession[key] as T;
}
if (this.knownNullishCacheKeys.has(key)) {
return null;
if (this.cache[key] !== undefined) {
return this.cache[key] as T;
}
return await this.getBypassCache(key, options);
}
async getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T> {
const session = await this.getLocalSession(await this.getSessionEncKey());
if (session[key] == null) {
this.knownNullishCacheKeys.add(key);
return null;
}
let value = await this.getLocalSessionValue(await this.sessionKey.get(), key);
let value = session[key];
if (options?.deserializer != null) {
value = options.deserializer(value as Jsonify<T>);
}
void this.save(key, value);
this.cache[key] = value;
return value as T;
}
@ -114,7 +91,7 @@ export class LocalBackedSessionStorageService
async save<T>(key: string, obj: T): Promise<void> {
// This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same.
if (this.platformUtilsService.isDev()) {
const existingValue = this.cachedSession[key] as T;
const existingValue = this.cache[key] as T;
if (this.compareValues<T>(existingValue, obj)) {
this.logService.warning(`Possible unnecessary write to local session storage. Key: ${key}`);
this.logService.warning(obj as any);
@ -125,128 +102,42 @@ export class LocalBackedSessionStorageService
return await this.remove(key);
}
this.knownNullishCacheKeys.delete(key);
this.cachedSession[key] = obj;
this.cache[key] = obj;
await this.updateLocalSessionValue(key, obj);
this.updatesSubject.next({ key, updateType: "save" });
}
async remove(key: string): Promise<void> {
this.knownNullishCacheKeys.add(key);
delete this.cachedSession[key];
this.cache[key] = null;
await this.updateLocalSessionValue(key, null);
this.updatesSubject.next({ key, updateType: "remove" });
}
private async updateLocalSessionValue<T>(key: string, obj: T) {
const sessionEncKey = await this.getSessionEncKey();
const localSession = (await this.getLocalSession(sessionEncKey)) ?? {};
localSession[key] = obj;
void this.setLocalSession(localSession, sessionEncKey);
}
async getLocalSession(encKey: SymmetricCryptoKey): Promise<Record<string, unknown>> {
if (Object.keys(this.cachedSession).length > 0) {
return this.cachedSession;
}
this.cachedSession = {};
const local = await this.localStorage.get<string>(this.sessionKey);
private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise<unknown> {
const local = await this.localStorage.get<string>(this.sessionStorageKey(key));
if (local == null) {
return this.cachedSession;
return null;
}
if (devFlagEnabled("storeSessionDecrypted")) {
return local as any as Record<string, unknown>;
const valueJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey);
if (valueJson == null) {
// error with decryption, value is lost, delete state and start over
await this.localStorage.remove(this.sessionStorageKey(key));
return null;
}
const sessionJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey);
if (sessionJson == null) {
// Error with decryption -- session is lost, delete state and key and start over
await this.setSessionEncKey(null);
await this.localStorage.remove(this.sessionKey);
return this.cachedSession;
}
this.cachedSession = JSON.parse(sessionJson);
return this.cachedSession;
return JSON.parse(valueJson);
}
async setLocalSession(session: Record<string, unknown>, key: SymmetricCryptoKey) {
if (devFlagEnabled("storeSessionDecrypted")) {
await this.setDecryptedLocalSession(session);
} else {
await this.setEncryptedLocalSession(session, key);
}
}
@devFlag("storeSessionDecrypted")
async setDecryptedLocalSession(session: Record<string, unknown>): Promise<void> {
// Make sure we're storing the jsonified version of the session
const jsonSession = JSON.parse(JSON.stringify(session));
if (session == null) {
await this.localStorage.remove(this.sessionKey);
} else {
await this.localStorage.save(this.sessionKey, jsonSession);
}
}
async setEncryptedLocalSession(session: Record<string, unknown>, key: SymmetricCryptoKey) {
const jsonSession = JSON.stringify(session);
const encSession = await this.encryptService.encrypt(jsonSession, key);
if (encSession == null) {
return await this.localStorage.remove(this.sessionKey);
}
await this.localStorage.save(this.sessionKey, encSession.encryptedString);
}
async getSessionEncKey(): Promise<SymmetricCryptoKey> {
let storedKey = await this.sessionStorage.get<SymmetricCryptoKey>(this.encKey);
if (storedKey == null || Object.keys(storedKey).length == 0) {
const generatedKey = await this.keyGenerationService.createKeyWithPurpose(
128,
"ephemeral",
"bitwarden-ephemeral",
);
storedKey = generatedKey.derivedKey;
await this.setSessionEncKey(storedKey);
return storedKey;
} else {
return SymmetricCryptoKey.fromJSON(storedKey);
}
}
async setSessionEncKey(input: SymmetricCryptoKey): Promise<void> {
if (input == null) {
await this.sessionStorage.remove(this.encKey);
} else {
await this.sessionStorage.save(this.encKey, input);
}
}
private compareValues<T>(value1: T, value2: T): boolean {
if (value1 == null && value2 == null) {
return true;
private async updateLocalSessionValue(key: string, value: unknown): Promise<void> {
if (value == null) {
await this.localStorage.remove(this.sessionStorageKey(key));
return;
}
if (value1 && value2 == null) {
return false;
}
if (value1 == null && value2) {
return false;
}
if (typeof value1 !== "object" || typeof value2 !== "object") {
return value1 === value2;
}
if (JSON.stringify(value1) === JSON.stringify(value2)) {
return true;
}
return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString();
const valueJson = JSON.stringify(value);
const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get());
await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString);
}
private async onMessageFromForeground(
@ -282,7 +173,7 @@ export class LocalBackedSessionStorageService
}
protected broadcastMessage(data: Omit<MemoryStoragePortMessage, "originator">) {
this._ports.forEach((port) => {
this.ports.forEach((port) => {
this.sendMessageTo(port, data);
});
}
@ -296,4 +187,32 @@ export class LocalBackedSessionStorageService
originator: "background",
});
}
private sessionStorageKey(key: string) {
return `session_${key}`;
}
private compareValues<T>(value1: T, value2: T): boolean {
if (value1 == null && value2 == null) {
return true;
}
if (value1 && value2 == null) {
return false;
}
if (value1 == null && value2) {
return false;
}
if (typeof value1 !== "object" || typeof value2 !== "object") {
return value1 === value2;
}
if (JSON.stringify(value1) === JSON.stringify(value2)) {
return true;
}
return Object.entries(value1).sort().toString() === Object.entries(value2).sort().toString();
}
}

View File

@ -1,5 +1,9 @@
import { Observable } from "rxjs";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@ -12,11 +16,14 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: TDeps,
storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState<TTo> {
const [cacheKey, storageService] = storageLocation;
return new BackgroundDerivedState(
parentState$,
deriveDefinition,
this.memoryStorage,
storageService,
cacheKey,
dependencies,
);
}

View File

@ -23,10 +23,10 @@ export class BackgroundDerivedState<
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
memoryStorage: AbstractStorageService & ObservableStorageService,
portName: string,
dependencies: TDeps,
) {
super(parentState$, deriveDefinition, memoryStorage, dependencies);
const portName = deriveDefinition.buildCacheKey();
// listen for foreground derived states to connect
BrowserApi.addListener(chrome.runtime.onConnect, (port) => {

View File

@ -38,14 +38,21 @@ describe("foreground background derived state interactions", () => {
let memoryStorage: FakeStorageService;
const initialParent = "2020-01-01";
const ngZone = mock<NgZone>();
const portName = "testPort";
beforeEach(() => {
mockPorts();
parentState$ = new Subject<string>();
memoryStorage = new FakeStorageService();
background = new BackgroundDerivedState(parentState$, deriveDefinition, memoryStorage, {});
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone);
background = new BackgroundDerivedState(
parentState$,
deriveDefinition,
memoryStorage,
portName,
{},
);
foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
});
afterEach(() => {
@ -65,7 +72,12 @@ describe("foreground background derived state interactions", () => {
});
it("should initialize a late-connected foreground", async () => {
const newForeground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone);
const newForeground = new ForegroundDerivedState(
deriveDefinition,
memoryStorage,
portName,
ngZone,
);
const backgroundEmissions = trackEmissions(background.state$);
parentState$.next(initialParent);
await awaitAsync();
@ -82,8 +94,6 @@ describe("foreground background derived state interactions", () => {
const dateString = "2020-12-12";
const emissions = trackEmissions(background.state$);
// 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
await foreground.forceValue(new Date(dateString));
await awaitAsync();
@ -99,9 +109,7 @@ describe("foreground background derived state interactions", () => {
expect(foreground["port"]).toBeDefined();
const newDate = new Date();
// 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
foreground.forceValue(newDate);
await foreground.forceValue(newDate);
await awaitAsync();
expect(connectMock.mock.calls.length).toBe(initialConnectCalls);
@ -114,9 +122,7 @@ describe("foreground background derived state interactions", () => {
expect(foreground["port"]).toBeUndefined();
const newDate = new Date();
// 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
foreground.forceValue(newDate);
await foreground.forceValue(newDate);
await awaitAsync();
expect(connectMock.mock.calls.length).toBe(initialConnectCalls + 1);

View File

@ -5,6 +5,7 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- extending this class for this client
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
@ -14,16 +15,18 @@ import { ForegroundDerivedState } from "./foreground-derived-state";
export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider {
constructor(
memoryStorage: AbstractStorageService & ObservableStorageService,
storageServiceProvider: StorageServiceProvider,
private ngZone: NgZone,
) {
super(memoryStorage);
super(storageServiceProvider);
}
override buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
_parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
_dependencies: TDeps,
storageLocation: [string, AbstractStorageService & ObservableStorageService],
): DerivedState<TTo> {
return new ForegroundDerivedState(deriveDefinition, this.memoryStorage, this.ngZone);
const [cacheKey, storageService] = storageLocation;
return new ForegroundDerivedState(deriveDefinition, storageService, cacheKey, this.ngZone);
}
}

View File

@ -33,13 +33,14 @@ jest.mock("../browser/run-inside-angular.operator", () => {
describe("ForegroundDerivedState", () => {
let sut: ForegroundDerivedState<Date>;
let memoryStorage: FakeStorageService;
const portName = "testPort";
const ngZone = mock<NgZone>();
beforeEach(() => {
memoryStorage = new FakeStorageService();
memoryStorage.internalUpdateValuesRequireDeserialization(true);
mockPorts();
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone);
sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, portName, ngZone);
});
afterEach(() => {

View File

@ -35,6 +35,7 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
constructor(
private deriveDefinition: DeriveDefinition<unknown, TTo, DerivedStateDependencies>,
private memoryStorage: AbstractStorageService & ObservableStorageService,
private portName: string,
private ngZone: NgZone,
) {
this.storageKey = deriveDefinition.storageKey;
@ -88,7 +89,7 @@ export class ForegroundDerivedState<TTo> implements DerivedState<TTo> {
return;
}
this.port = chrome.runtime.connect({ name: this.deriveDefinition.buildCacheKey() });
this.port = chrome.runtime.connect({ name: this.portName });
this.backgroundResponses$ = fromChromeEvent(this.port.onMessage).pipe(
map(([message]) => message as DerivedStateMessage),

View File

@ -0,0 +1,35 @@
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import {
PossibleLocation,
StorageServiceProvider,
} from "@bitwarden/common/platform/services/storage-service.provider";
// eslint-disable-next-line import/no-restricted-paths
import { ClientLocations } from "@bitwarden/common/platform/state/state-definition";
export class BrowserStorageServiceProvider extends StorageServiceProvider {
constructor(
diskStorageService: AbstractStorageService & ObservableStorageService,
limitedMemoryStorageService: AbstractStorageService & ObservableStorageService,
private largeObjectMemoryStorageService: AbstractStorageService & ObservableStorageService,
) {
super(diskStorageService, limitedMemoryStorageService);
}
override get(
defaultLocation: PossibleLocation,
overrides: Partial<ClientLocations>,
): [location: PossibleLocation, service: AbstractStorageService & ObservableStorageService] {
const location = overrides["browser"] ?? defaultLocation;
switch (location) {
case "memory-large-object":
return ["memory-large-object", this.largeObjectMemoryStorageService];
default:
// Pass in computed location to super because they could have
// override default "disk" with web "memory".
return super.get(location, overrides);
}
}
}

View File

@ -71,6 +71,7 @@ import { GlobalState } from "@bitwarden/common/platform/models/domain/global-sta
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service";
import {
DerivedStateProvider,
@ -108,6 +109,7 @@ import { DefaultBrowserStateService } from "../../platform/services/default-brow
import I18nService from "../../platform/services/i18n.service";
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider";
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging";
import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service";
@ -120,6 +122,10 @@ import { InitService } from "./init.service";
import { PopupCloseWarningService } from "./popup-close-warning.service";
import { PopupSearchService } from "./popup-search.service";
const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken<
AbstractStorageService & ObservableStorageService
>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE");
const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired();
const isPrivateMode = BrowserPopupUtils.inPrivateMode();
const mainBackground: MainBackground = needsBackgroundInit
@ -380,6 +386,21 @@ const safeProviders: SafeProvider[] = [
},
deps: [],
}),
safeProvider({
provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
useFactory: (
regularMemoryStorageService: AbstractMemoryStorageService & ObservableStorageService,
) => {
if (BrowserApi.isManifestVersion(2)) {
return regularMemoryStorageService;
}
return getBgService<AbstractStorageService & ObservableStorageService>(
"largeObjectMemoryStorageForStateProviders",
)();
},
deps: [OBSERVABLE_MEMORY_STORAGE],
}),
safeProvider({
provide: OBSERVABLE_DISK_STORAGE,
useExisting: AbstractStorageService,
@ -466,7 +487,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({
provide: DerivedStateProvider,
useClass: ForegroundDerivedStateProvider,
deps: [OBSERVABLE_MEMORY_STORAGE, NgZone],
deps: [StorageServiceProvider, NgZone],
}),
safeProvider({
provide: AutofillSettingsServiceAbstraction,
@ -542,6 +563,15 @@ const safeProviders: SafeProvider[] = [
},
deps: [],
}),
safeProvider({
provide: StorageServiceProvider,
useClass: BrowserStorageServiceProvider,
deps: [
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE,
],
}),
];
@NgModule({

View File

@ -121,55 +121,55 @@
<value>Bitwarden Passwort-Manager</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>Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.</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>Ausgezeichnet als bester Passwortmanager von PCMag, WIRED, The Verge, CNET, G2 und vielen anderen!
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.
SCHÜTZE DEIN DIGITALES LEBEN
Sicher dein digitales Leben und schütze dich vor Passwortdiebstählen, indem du individuelle, sichere Passwörter für jedes Konto erstellest und speicherst. Verwalte alles in einem Ende-zu-Ende verschlüsselten Passwort-Tresor, auf den nur du Zugriff hast.
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
ZUGRIFF AUF DEINE DATEN, ÜBERALL, JEDERZEIT UND AUF JEDEM GERÄT
Verwalte, speichere, sichere und teile einfach eine unbegrenzte Anzahl von Passwörtern auf einer unbegrenzten Anzahl von Geräten ohne Einschränkungen.
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.
JEDER SOLLTE DIE MÖGLICHKEIT HABEN, ONLINE GESCHÜTZT ZU BLEIBEN
Verwende Bitwarden kostenlos, ohne Werbung oder Datenverkauf. Bitwarden glaubt, dass jeder die Möglichkeit haben sollte, online geschützt zu bleiben. Premium-Abos bieten Zugang zu erweiterten Funktionen.
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.
STÄRKE DEINE TEAMS MIT BITWARDEN
Tarife für Teams und Enterprise enthalten professionelle Business-Funktionen. Einige Beispiele sind SSO-Integration, Selbst-Hosting, Directory-Integration und SCIM-Bereitstellung, globale Richtlinien, API-Zugang, Ereignisprotokolle und mehr.
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
Nutze Bitwarden, um deine Mitarbeiter abzusichern und sensible Informationen mit Kollegen zu teilen.
More reasons to choose Bitwarden:
Weitere Gründe, Bitwarden zu wählen:
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.
Weltklasse-Verschlüsselung
Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashtag und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben.
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.
3rd-Party-Prüfungen
Bitwarden führt regelmäßig umfassende Sicherheitsprüfungen durch Dritte von namhaften Sicherheitsfirmen durch. Diese jährlichen Prüfungen umfassen Quellcode-Bewertungen und Penetration-Tests für Bitwarden-IPs, Server und Webanwendungen.
Advanced 2FA
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
Erweiterte 2FA
Schütze deine Zugangsdaten mit einem Authentifikator eines Drittanbieters, per E-Mail verschickten Codes oder FIDO2 WebAuthn-Zugangsadaten wie einem Hardware-Sicherheitsschlüssel oder Passkey.
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
Übertrage Daten direkt an andere, während die Ende-zu-Ende-Verschlüsselung beibehalten wird und die Verbreitung begrenzt werden kann.
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.
Eingebauter Generator
Erstelle lange, komplexe und eindeutige Passwörter und eindeutige Benutzernamen für jede Website, die du besuchst. Integriere E-Mail-Alias-Anbieter für zusätzlichen Datenschutz.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Globale Übersetzungen
Es gibt Bitwarden-Übersetzungen für mehr als 60 Sprachen, die von der weltweiten Community über Crowdin übersetzt werden.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Plattformübergreifende Anwendungen
Schütze und teile sensible Daten in deinem Bitwarden Tresor von jedem Browser, mobilen Gerät oder Desktop-Betriebssystem und mehr.
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!
Bitwarden schützt mehr als nur Passwörter
Ende-zu-Ende verschlüsselte Zugangsverwaltungs-Lösungen von Bitwarden ermöglicht es Organisationen, alles zu sichern, einschließlich Entwicklergeheimnissen und Passkeys. Besuche Bitwarden.com, um mehr über den Bitwarden Secrets Manager und Bitwarden Passwordless.dev zu erfahren!
</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>Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu</value>

View File

@ -121,7 +121,7 @@
<value>Bitwarden Password Manager</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>집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.</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>집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>여러 기기에서 보관함에 접근하고 동기화할 수 있습니다.</value>

View File

@ -118,58 +118,58 @@
<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>Menedżer Haseł Bitwarden</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>W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.</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>Uznany za najlepszego menedżera haseł przez PCMag, WIRED, The Verge, CNET, G2 i wielu innych!
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.
ZABEZPIECZ SWOJE CYFROWE ŻYCIE
Zabezpiecz swoje cyfrowe życie i chroń przed naruszeniami danych, generując i zapisując unikalne, silne hasła do każdego konta. Przechowuj wszystko w zaszyfrowanym end-to-end magazynie haseł, do którego tylko Ty masz dostęp.
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
DOSTĘP DO SWOICH DANYCH W KAŻDYM MIEJSCU, W DOWOLNYM CZASIE, NA KAŻDYM URZĄDZENIU
Z łatwością zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń.
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.
KAŻDY POWINIEN POSIADAĆ NARZĘDZIA ABY ZACHOWAĆ BEZPIECZEŃSTWO W INTERNECIE
Korzystaj z Bitwarden za darmo, bez reklam i sprzedawania Twoich danych. Bitwarden wierzy, że każdy powinien mieć możliwość zachowania bezpieczeństwa w Internecie. Plany premium oferują dostęp do zaawansowanych funkcji.
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.
WZMOCNIJ SWOJE ZESPOŁY DZIĘKI BITWARDEN
Plany dla Zespołów i Enterprise oferują profesjonalne funkcje biznesowe. Na przykład obejmują integrację z SSO, własny hosting, integrację katalogów i udostępnianie SCIM, zasady globalne, dostęp do API, dzienniki zdarzeń i inne.
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
Użyj Bitwarden, aby zabezpieczyć swoich pracowników i udostępniać poufne informacje współpracownikom.
More reasons to choose Bitwarden:
Więcej powodów, aby wybrać 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.
Szyfrowanie na światowym poziomie
Hasła są chronione za pomocą zaawansowanego, kompleksowego szyfrowania (AES-256-bitowy, solony hashtag i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne.
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.
Audyty stron trzecich
Bitwarden regularnie przeprowadza kompleksowe audyty bezpieczeństwa stron trzecich we współpracy ze znanymi firmami security. Te coroczne audyty obejmują ocenę kodu źródłowego i testy penetracyjne adresów IP Bitwarden, serwerów i aplikacji internetowych.
Advanced 2FA
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
Zaawansowane 2FA
Zabezpiecz swój login za pomocą zewnętrznego narzędzia uwierzytelniającego, kodów przesłanych pocztą elektroniczną lub poświadczeń FIDO2 WebAuthn, takich jak sprzętowy klucz bezpieczeństwa lub hasło.
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
Bitwarden Wyślij
Przesyłaj dane bezpośrednio do innych, zachowując kompleksowe szyfrowane bezpieczeństwo i ograniczając ryzyko.
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.
Wbudowany generator
Twórz długie, złożone i różne hasła oraz unikalne nazwy użytkowników dla każdej odwiedzanej witryny. Zintegruj się z dostawcami aliasów e-mail, aby uzyskać dodatkową prywatność.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Tłumaczenia globalne
Istnieją tłumaczenia Bitwarden na ponad 60 języków, tłumaczone przez globalną społeczność za pośrednictwem Crowdin.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Aplikacje wieloplatformowe
Zabezpiecz i udostępniaj poufne dane w swoim Sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego lub systemu operacyjnego na komputerze stacjonarnym i nie tylko.
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!
Bitwarden zabezpiecza nie tylko hasła
Rozwiązania do zarządzania danymi zaszyfrownaymi end-to-end od firmy Bitwarden umożliwiają organizacjom zabezpieczanie wszystkiego, w tym tajemnic programistów i kluczy dostępu. Odwiedź Bitwarden.com, aby dowiedzieć się więcej o Mendżerze Sekretów Bitwarden i Bitwarden Passwordless.dev!
</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>W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Synchronizacja i dostęp do sejfu z różnych urządzeń</value>

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>Gerenciador de Senhas Bitwarden</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>Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.</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>Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Sincronize e acesse o seu cofre através de múltiplos dispositivos</value>

View File

@ -118,58 +118,56 @@
<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 密码管理器</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>无论是在家里、工作中还是在外出时Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。</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>被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳密码管理器!
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.
保护您的数字生活
通过为每个账户生成并保存独特而强大的密码,保护您的数字生活并防范数据泄露。所有内容保存在只有您可以访问的端对端加密的密码库中。
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.
每个人都应该拥有的保持在线安全的工具
使用 Bitwarden 是免费的没有广告不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了堆高级功能的访问。
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.
通过 BITWARDEN 为您的团队提供支持
团队和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成和 SCIM 配置、全局策略、API 访问、事件日志等。
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
使用 Bitwarden 保护您的团队,并与同事共享敏感信息。
选择 Bitwarden 的更多理由:
More reasons to choose Bitwarden:
世界级加密
密码受到先进的端对端加密AES-256 位、加盐哈希标签和 PBKDF2 SHA-256保护使您的数据保持安全和私密。
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.
第三方审计
Bitwarden 定期与知名的安全公司进行全面的第三方安全审计。这些年度审核包括对 Bitwarden IP、服务器和 Web 应用程序的源代码评估和渗透测试。
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
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
高级两步验证
使用第三方身份验证器、通过电子邮件发送代码或 FIDO2 WebAuthn 凭据(如硬件安全钥匙或通行密钥)保护您的登录。
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
直接传输数据给他人,同时保持端对端加密的安全性并防止曝露。
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 的翻译涵盖 60 多种语言,由全球社区通过 Crowdin 翻译。
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
跨平台应用程序
从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。
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!
</value>
Bitwarden 保护的不仅仅是密码
Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息!</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>无论是在家里、工作中还是在外出时Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>从多台设备同步和访问密码库</value>

View File

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

View File

@ -309,9 +309,7 @@ export class Main {
this.singleUserStateProvider,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(
this.memoryStorageForStateProviders,
);
this.derivedStateProvider = new DefaultDerivedStateProvider(storageServiceProvider);
this.stateProvider = new DefaultStateProvider(
this.activeUserStateProvider,
@ -633,7 +631,6 @@ export class Main {
this.avatarService,
async (expired: boolean) => await this.logout(),
this.billingAccountProfileStateService,
this.tokenService,
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);

View File

@ -228,7 +228,8 @@
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"snap": {
"summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.",
"summary": "Bitwarden is a secure and free password manager for all of your devices.",
"description": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.",
"autoStart": true,
"base": "core22",
"confinement": "strict",

View File

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

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "تنسيقات مشتركة",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Ortaq formatlar",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Problemlərin aradan qaldırılması"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Често използвани формати",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Отстраняване на проблеми"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Formats comuns",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Resolució de problemes"
},

View File

@ -2697,6 +2697,9 @@
"message": "Společné formáty",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Řešení problémů"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Almindelige formater",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Fejlfinding"
},

View File

@ -1633,10 +1633,10 @@
"message": "Browser-Integration wird nicht unterstützt"
},
"browserIntegrationErrorTitle": {
"message": "Error enabling browser integration"
"message": "Fehler beim Aktivieren der Browser-Integration"
},
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
"message": "Beim Aktivieren der Browser-Integration ist ein Fehler aufgetreten."
},
"browserIntegrationMasOnlyDesc": {
"message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt."
@ -2697,6 +2697,9 @@
"message": "Gängigste Formate",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Problembehandlung"
},

View File

@ -2697,6 +2697,9 @@
"message": "Κοινές μορφές",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Αντιμετώπιση Προβλημάτων"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "فرمت‌های رایج",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Yleiset muodot",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Vianetsintä"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Formats communs",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Résolution de problèmes"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "תסדירים נפוצים",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Általános formátumok",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Hibaelhárítás"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Formati comuni",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Successo"
},
"troubleshooting": {
"message": "Risoluzione problemi"
},

View File

@ -2697,6 +2697,9 @@
"message": "一般的な形式",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "成功"
},
"troubleshooting": {
"message": "トラブルシューティング"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Dažni formatai",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Izplatīti veidoli",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Sarežģījumu novēršana"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Vanlige formater",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Veelvoorkomende formaten",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Probleemoplossing"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Popularne formaty",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Sukces"
},
"troubleshooting": {
"message": "Rozwiązywanie problemów"
},

View File

@ -561,10 +561,10 @@
"message": "A sua nova conta foi criada! Agora você pode iniciar a sessão."
},
"youSuccessfullyLoggedIn": {
"message": "You successfully logged in"
"message": "Você logou na sua conta com sucesso"
},
"youMayCloseThisWindow": {
"message": "You may close this window"
"message": "Você pode fechar esta janela"
},
"masterPassSent": {
"message": "Enviamos um e-mail com a dica da sua senha mestra."
@ -801,10 +801,10 @@
"message": "Alterar Senha Mestra"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Continuar no aplicativo web?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden."
},
"fingerprintPhrase": {
"message": "Frase biométrica",
@ -1402,7 +1402,7 @@
"message": "Código PIN inválido."
},
"tooManyInvalidPinEntryAttemptsLoggingOut": {
"message": "Too many invalid PIN entry attempts. Logging out."
"message": "Muitas tentativas de entrada de PIN inválidas. Desconectando."
},
"unlockWithWindowsHello": {
"message": "Desbloquear com o Windows Hello"
@ -1557,7 +1557,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired": {
"message": "Verification required",
"message": "Verificação necessária",
"description": "Default title for the user verification dialog."
},
"currentMasterPass": {
@ -1633,10 +1633,10 @@
"message": "Integração com o navegador não suportado"
},
"browserIntegrationErrorTitle": {
"message": "Error enabling browser integration"
"message": "Erro ao ativar a integração do navegador"
},
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
"message": "Ocorreu um erro ao permitir a integração do navegador."
},
"browserIntegrationMasOnlyDesc": {
"message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store."
@ -1654,10 +1654,10 @@
"message": "Ative uma camada adicional de segurança, exigindo validação de frase de impressão digital ao estabelecer uma ligação entre o computador e o navegador. Quando ativado, isto requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida."
},
"enableHardwareAcceleration": {
"message": "Use hardware acceleration"
"message": "Utilizar aceleração de hardware"
},
"enableHardwareAccelerationDesc": {
"message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required."
"message": "Por padrão esta configuração está ativada. Desligar apenas se tiver problemas gráficos. Reiniciar é necessário."
},
"approve": {
"message": "Aprovar"
@ -1898,40 +1898,40 @@
"message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora."
},
"tryAgain": {
"message": "Try again"
"message": "Tentar novamente"
},
"verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue."
"message": "Verificação necessária para esta ação. Defina um PIN para continuar."
},
"setPin": {
"message": "Set PIN"
"message": "Definir PIN"
},
"verifyWithBiometrics": {
"message": "Verify with biometrics"
"message": "Verificiar com biometria"
},
"awaitingConfirmation": {
"message": "Awaiting confirmation"
"message": "Aguardando confirmação"
},
"couldNotCompleteBiometrics": {
"message": "Could not complete biometrics."
"message": "Não foi possível completar a biometria."
},
"needADifferentMethod": {
"message": "Need a different method?"
"message": "Precisa de um método diferente?"
},
"useMasterPassword": {
"message": "Use master password"
"message": "Usar a senha mestra"
},
"usePin": {
"message": "Use PIN"
"message": "Usar PIN"
},
"useBiometrics": {
"message": "Use biometrics"
"message": "Usar biometria"
},
"enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email."
"message": "Digite o código de verificação que foi enviado para o seu e-mail."
},
"resendCode": {
"message": "Resend code"
"message": "Reenviar código"
},
"hours": {
"message": "Horas"
@ -2541,13 +2541,13 @@
}
},
"launchDuoAndFollowStepsToFinishLoggingIn": {
"message": "Launch Duo and follow the steps to finish logging in."
"message": "Inicie o Duo e siga os passos para finalizar o login."
},
"duoRequiredByOrgForAccount": {
"message": "Duo two-step login is required for your account."
"message": "A autenticação em duas etapas do Duo é necessária para sua conta."
},
"launchDuo": {
"message": "Launch Duo in Browser"
"message": "Iniciar o Duo no navegador"
},
"importFormatError": {
"message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente."
@ -2630,13 +2630,13 @@
"message": "Nome de usuário ou senha incorretos"
},
"incorrectPassword": {
"message": "Incorrect password"
"message": "Senha incorreta"
},
"incorrectCode": {
"message": "Incorrect code"
"message": "Código incorreto"
},
"incorrectPin": {
"message": "Incorrect PIN"
"message": "PIN incorreto"
},
"multifactorAuthenticationFailed": {
"message": "Falha na autenticação de múltiplos fatores"
@ -2697,25 +2697,28 @@
"message": "Formatos comuns",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
"message": "Solução de problemas"
},
"disableHardwareAccelerationRestart": {
"message": "Disable hardware acceleration and restart"
"message": "Desativar aceleração de hardware e reiniciar"
},
"enableHardwareAccelerationRestart": {
"message": "Enable hardware acceleration and restart"
"message": "Ativar aceleração de hardware e reiniciar"
},
"removePasskey": {
"message": "Remove passkey"
"message": "Remover senha"
},
"passkeyRemoved": {
"message": "Passkey removed"
"message": "Chave de acesso removida"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Erro ao atribuir coleção de destino."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Erro ao atribuir pasta de destino."
}
}

View File

@ -2697,6 +2697,9 @@
"message": "Formatos comuns",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Resolução de problemas"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Основные форматы",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Успешно"
},
"troubleshooting": {
"message": "Устранение проблем"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Bežné formáty",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Riešenie problémov"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -801,10 +801,10 @@
"message": "Промени главну лозинку"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "Ићи на веб апликацију?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "Можете променити главну лозинку на Bitwarden веб апликацији."
},
"fingerprintPhrase": {
"message": "Сигурносна Фраза Сефа",
@ -1633,10 +1633,10 @@
"message": "Интеграција са претраживачем није подржана"
},
"browserIntegrationErrorTitle": {
"message": "Error enabling browser integration"
"message": "Грешка при омогућавању интеграције прегледача"
},
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
"message": "Дошло је до грешке при омогућавању интеграције прегледача."
},
"browserIntegrationMasOnlyDesc": {
"message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store."
@ -2697,6 +2697,9 @@
"message": "Уобичајени формати",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Решавање проблема"
},
@ -2713,9 +2716,9 @@
"message": "Приступачни кључ је уклоњен"
},
"errorAssigningTargetCollection": {
"message": "Error assigning target collection."
"message": "Грешка при додељивању циљне колекције."
},
"errorAssigningTargetFolder": {
"message": "Error assigning target folder."
"message": "Грешка при додељивању циљне фасцикле."
}
}

View File

@ -2697,6 +2697,9 @@
"message": "Vanliga format",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Felsökning"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Sorun giderme"
},

View File

@ -2697,6 +2697,9 @@
"message": "Поширені формати",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Успішно"
},
"troubleshooting": {
"message": "Усунення проблем"
},

View File

@ -2697,6 +2697,9 @@
"message": "Common formats",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "Troubleshooting"
},

View File

@ -801,10 +801,10 @@
"message": "修改主密码"
},
"continueToWebApp": {
"message": "Continue to web app?"
"message": "前往网页 App 吗?"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "You can change your master password on the Bitwarden web app."
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
},
"fingerprintPhrase": {
"message": "指纹短语",
@ -2697,6 +2697,9 @@
"message": "常规格式",
"description": "Label indicating the most common import formats"
},
"success": {
"message": "Success"
},
"troubleshooting": {
"message": "故障排除"
},

Some files were not shown because too many files have changed in this diff Show More