From f72ef2dce5cee26a4df80d3c6d23ccddb4b2fe4a Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Wed, 21 Sep 2022 09:40:23 -0400 Subject: [PATCH 01/23] Don't try and parse a json response if one is not received (#3574) --- libs/common/src/services/api.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index dbc67f57272..0b96a11831c 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -2323,7 +2323,9 @@ export class ApiService implements ApiServiceAbstraction { requestInit.headers = headers; const response = await this.fetch(new Request(requestUrl, requestInit)); - if (hasResponse && response.status === 200) { + const responseType = response.headers.get("content-type"); + const responseIsJson = responseType != null && responseType.indexOf("application/json") !== -1; + if (hasResponse && response.status === 200 && responseIsJson) { const responseJson = await response.json(); return responseJson; } else if (response.status !== 200) { From 144d0a8beb88e353c1783ba7e02757c1d870cfb3 Mon Sep 17 00:00:00 2001 From: ATJB <71517198+atjbramley@users.noreply.github.com> Date: Wed, 21 Sep 2022 16:48:17 +0200 Subject: [PATCH 02/23] Corrected help command string (#3560) --- apps/cli/src/send.program.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/cli/src/send.program.ts b/apps/cli/src/send.program.ts index 69e5eeaedf3..55780ad4483 100644 --- a/apps/cli/src/send.program.ts +++ b/apps/cli/src/send.program.ts @@ -139,7 +139,7 @@ export class SendProgram extends Program { return new program.Command("template") .arguments("") .description("Get json templates for send objects", { - object: "Valid objects are: send, send.text, send.file", + object: "Valid objects are: send.text, send.file", }) .action(async (object) => { const cmd = new GetCommand( From 8cc54181fc3f05fc6443787cf6ac92fcc0236383 Mon Sep 17 00:00:00 2001 From: ATJB <71517198+atjbramley@users.noreply.github.com> Date: Wed, 21 Sep 2022 17:43:23 +0200 Subject: [PATCH 03/23] Correct bw send get -h internal CLI documentation (#3576) --- apps/cli/src/send.program.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/cli/src/send.program.ts b/apps/cli/src/send.program.ts index 55780ad4483..f92e2a305e1 100644 --- a/apps/cli/src/send.program.ts +++ b/apps/cli/src/send.program.ts @@ -176,12 +176,12 @@ export class SendProgram extends Program { writeLn(""); writeLn(" Examples:"); writeLn(""); - writeLn(" bw get send searchText"); - writeLn(" bw get send id"); - writeLn(" bw get send searchText --text"); - writeLn(" bw get send searchText --file"); - writeLn(" bw get send searchText --file --output ../Photos/photo.jpg"); - writeLn(" bw get send searchText --file --raw"); + writeLn(" bw send get searchText"); + writeLn(" bw send get id"); + writeLn(" bw send get searchText --text"); + writeLn(" bw send get searchText --file"); + writeLn(" bw send get searchText --file --output ../Photos/photo.jpg"); + writeLn(" bw send get searchText --file --raw"); writeLn("", true); }) .action(async (id: string, options: program.OptionValues) => { From 73b95cf8c4de6d8ca2efb8b68519dc49d54be7b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:05:10 +0200 Subject: [PATCH 04/23] Autosync the updated translations (#3583) Co-authored-by: github-actions <> --- apps/web/src/locales/be/messages.json | 306 +++++++++++++------------- apps/web/src/locales/bg/messages.json | 2 +- apps/web/src/locales/el/messages.json | 20 +- apps/web/src/locales/fi/messages.json | 22 +- apps/web/src/locales/lv/messages.json | 30 +-- apps/web/src/locales/uk/messages.json | 4 +- 6 files changed, 192 insertions(+), 192 deletions(-) diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index b0ce295735f..a140a225414 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -10,7 +10,7 @@ } }, "whatTypeOfItem": { - "message": "Выберыце тып элемента" + "message": "Які гэта элемент запісу?" }, "name": { "message": "Назва" @@ -137,10 +137,10 @@ "message": "Доктар" }, "expirationMonth": { - "message": "Месяц заканчэння" + "message": "Месяц завяршэння" }, "expirationYear": { - "message": "Год заканчэння" + "message": "Год завяршэння" }, "authenticatorKeyTotp": { "message": "Ключ аўтэнтыфікацыі (TOTP)" @@ -227,7 +227,7 @@ "description": "Toggling an expand/collapse state." }, "generatePassword": { - "message": "Згенерыраваць пароль" + "message": "Генерыраваць пароль" }, "checkPassword": { "message": "Праверце, ці не скампраметаваны пароль." @@ -294,7 +294,7 @@ "message": "Тыпы" }, "typeLogin": { - "message": "Імя карыстальніка" + "message": "Лагін" }, "typeCard": { "message": "Картка" @@ -315,7 +315,7 @@ "message": "Пасведчанні" }, "typeSecureNotePlural": { - "message": "Бяспечныя нататкі" + "message": "Абароненыя нататкі" }, "folders": { "message": "Папкі" @@ -369,10 +369,10 @@ "message": "Дадаць элемент" }, "editItem": { - "message": "Рэдагаванне элемента" + "message": "Рэдагаваць элемент" }, "viewItem": { - "message": "Прагляд элемента" + "message": "Прагледзець элемент" }, "ex": { "message": "напр.", @@ -549,7 +549,7 @@ "message": "Вы выйшлі" }, "loginExpired": { - "message": "Скончыўся тэрмін дзеяння вашага сеансу." + "message": "Тэрмін дзеяння вашага сеансу завяршыўся." }, "logOutConfirmation": { "message": "Вы ўпэўнены, што хочаце выйсці?" @@ -621,7 +621,7 @@ "message": "Увядзіце адрас электроннай пошты ўліковага запісу для атрымання падказкі да асноўнага пароля." }, "getMasterPasswordHint": { - "message": "Атрымаць падказку для асноўнага пароля" + "message": "Атрымаць падказку да асноўнага пароля" }, "emailRequired": { "message": "Патрабуецца адрас электроннай пошты." @@ -718,7 +718,7 @@ "message": "Увядзіце 6 лічбаў праверачнага кода з вашай праграмы аўтэнтыфікацыі." }, "enterVerificationCodeEmail": { - "message": "Увядзіце 6 лічбаў кода праверкі, які быў адпраўлены на $EMAIL$.", + "message": "Увядзіце 6 лічбаў праверачнага кода, які быў адпраўлены на $EMAIL$.", "placeholders": { "email": { "content": "$1", @@ -782,11 +782,11 @@ "message": "Выкарыстоўвайце YubiKey для доступу да вашага ўліковага запісу. Працуе з прыладамі YubiKey серый 4, 5 і NEO." }, "duoDesc": { - "message": "Пацвярдзіце з дапамогай Duo Security, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі.", + "message": "Праверка з дапамогай Duo Security, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { - "message": "Пацвярдзіце з дапамогай Duo Security для вашай арганізацыі, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі.", + "message": "Праверка з дапамогай Duo Security для вашай арганізацыі, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "u2fDesc": { @@ -829,7 +829,7 @@ "message": "Рэдагуйце калекцыі, з якімі гэты элемент знаходзіцца ў агульным доступе. Толькі карыстальнікі арганізацыі з доступам да гэтых калекцый змогуць бачыць гэты элемент." }, "deleteSelectedItemsDesc": { - "message": "Вы выбралі наступную колькасць запісаў для выдалення: $COUNT$. Вы ўпэўнены, што хочаце выдаліць гэтыя элементы?", + "message": "Вы выбралі наступную колькасць элементаў для выдалення: $COUNT$ шт. Вы ўпэўнены, што хочаце выдаліць іх?", "placeholders": { "count": { "content": "$1", @@ -847,7 +847,7 @@ } }, "moveSelectedItemsCountDesc": { - "message": "Вы выбралі наступную колькасць элементаў: $COUNT$. Наступная колькасць колькасць будзе перамешчана: $MOVEABLE_COUNT$. Застануцца без перамяшчэння: $NONMOVEABLE_COUNT$.", + "message": "Вы выбралі наступную колькасць элементаў: $COUNT$ шт. З іх будуць перамешчаны ў арганізацыю: $MOVEABLE_COUNT$ шт. Не перамешчанымі застануцца: $NONMOVEABLE_COUNT$ шт.", "placeholders": { "count": { "content": "$1", @@ -876,10 +876,10 @@ "message": "Пацвердзіць экспартаванне сховішча" }, "exportWarningDesc": { - "message": "Файл, які экспартуецца ўтрымлівае даныя вашага сховішча ў незашыфраваным фармаце. Яго не варта захоўваць або адпраўляць па неабароненых каналах (напрыклад, па электроннай пошце). Выдаліце яго адразу пасля выкарыстання." + "message": "Пры экспартаванні файл утрымлівае даныя вашага сховішча ў незашыфраваным фармаце. Яго не варта захоўваць або адпраўляць па неабароненых каналах (напрыклад, па электроннай пошце). Выдаліце яго адразу пасля выкарыстання." }, "encExportKeyWarningDesc": { - "message": "Пры экспарце даныя шыфруюцца з дапамогай ключа шыфравання ўліковага запісу. Калі вы калі-небудзь зменіце ключ шыфравання ўліковага запісу, вам неабходна будзе экспартаваць даныя паўторна, паколькі вы не зможаце расшыфраваць гэты файл экспартавання." + "message": "Пры экспартаванні даныя шыфруюцца з дапамогай ключа шыфравання ўліковага запісу. Калі вы калі-небудзь зменіце ключ шыфравання ўліковага запісу, вам неабходна будзе экспартаваць даныя паўторна, паколькі вы не зможаце расшыфраваць гэты файл экспартавання." }, "encExportAccountWarningDesc": { "message": "Ключы шыфравання з'яўляюцца ўнікальнымі для кожнага ўліковага запісу Bitwarden, таму нельга імпартаваць зашыфраванае сховішча ў іншы ўліковы запіс." @@ -955,7 +955,7 @@ "message": "Пазбягаць неадназначных сімвалаў" }, "regeneratePassword": { - "message": "Згенерыраваць новы пароль" + "message": "Паўторна генерыраваць пароль" }, "length": { "message": "Даўжыня" @@ -1013,7 +1013,7 @@ "message": "Код" }, "changeEmailDesc": { - "message": "Мы адправілі праверачны код на $EMAIL$. Праверце сваю пошту на наяўнасць гэтага кода і ўвядзіце яго ніжэй, каб завяршыць змяненне адраса электроннай пошты.", + "message": "На ваш паштовы адрас $EMAIL$ адпраўлены ліст з праверачным кодам. Праверце сваю пошту на наяўнасць гэтага кода і ўвядзіце яго ніжэй, каб завяршыць змяненне адраса электроннай пошты.", "placeholders": { "email": { "content": "$1", @@ -1028,10 +1028,10 @@ "message": "Электронная пошта зменена" }, "logBackIn": { - "message": "Калі ласка, увайдзіце зноў." + "message": "Калі ласка, увайдзіце паўторна." }, "logBackInOthersToo": { - "message": "Калі ласка, увайдзіце паўторна. Калі вы выкарыстоўваеце іншыя праграмы Bitwarden, выйдзіце з іх, а потым увайдзіце зноў." + "message": "Калі ласка, увайдзіце паўторна. Калі вы выкарыстоўваеце іншыя праграмы Bitwarden, выйдзіце з іх, а потым увайдзіце яшчэ раз." }, "changeMasterPassword": { "message": "Змяніць асноўны пароль" @@ -1067,7 +1067,7 @@ } }, "kdfIterationsWarning": { - "message": "Занадта вялікае значэнне ітэрацый KDF можа істотна запаволіць уваход (і разблакіроўку) на прыладах з састарэлымі працэсарамі. Мы рэкамендуем паслядоўна павялічваць значэнне з крокам $INCREMENT$ і правяраць вынік на ўсіх вашых прыладах.", + "message": "Занадта вялікае значэнне ітэрацый KDF можа істотна запаволіць уваход (і разблакіроўку) на прыладах, якія маюць састарэлы працэсар. Мы рэкамендуем паслядоўна павялічваць значэнне з крокам $INCREMENT$ і правяраць вынік на ўсіх вашых прыладах.", "placeholders": { "increment": { "content": "$1", @@ -1148,7 +1148,7 @@ "message": "Памылка імпартавання" }, "importErrorDesc": { - "message": "Адбылася праблема з данымі, якія вы спрабуеце імпартаваць. Калі ласка, выпраўце памылкі, якія пералічаны ў вашым зыходным файле і паспрабуйце яшчэ раз." + "message": "Адбылася праблема з данымі, якія вы спрабуеце імпартаваць. Калі ласка, выпраўце памылкі, якія пералічаны ніжэй у вашым зыходным файле і паспрабуйце яшчэ раз." }, "importSuccess": { "message": "Даныя былі паспяхова імпартаваны ў ваша сховішча." @@ -1212,7 +1212,7 @@ "message": "Паказваць значкі вэб-сайтаў" }, "faviconDesc": { - "message": "Паказваць распазнавальны відарыс побач з кожным з кожным лагінам." + "message": "Паказваць распазнавальны відарыс побач з кожным лагінам." }, "enableGravatars": { "message": "Паказваць Gravatars", @@ -1235,7 +1235,7 @@ "message": "Правілы дамена" }, "domainRulesDesc": { - "message": "Калі ў вас ёсць аднолькавы лагін на некалькіх розных даменах вэб-сайта, то вы можаце пазначыць вэб-сайт як \"эквівалентны\". \"Глабальныя\" - гэта дамены, які створаны для вас Bitwarden." + "message": "Калі ў вас ёсць аднолькавы лагін на некалькіх розных даменах вэб-сайта, вы можаце пазначыць вэб-сайт як \"эквівалентны\". \"Глабальныя\" - гэта дамены, якія стварыў для вас Bitwarden." }, "globalEqDomains": { "message": "Глабальныя эквівалентныя дамены" @@ -1280,7 +1280,7 @@ "message": "Патрабаваць двухэтапны ўваход для карыстальнікаў вашай арганізацыі, які сканфігураваны на ўзроўні арганізацыі." }, "twoStepLoginRecoveryWarning": { - "message": "Уключэнне двухэтапнага ўваходу можа цалкам заблакіраваць доступ да ўліковага запісу Bitwarden. Код аднаўлення дае магчымасць атрымаць доступ да вашага ўліковага запісу ў выпадку, калі вы не можаце скарыстацца звычайным спосабам пастаўшчыка двухэтапнага ўваходу (напрыклад, вы згубілі сваю прыладу). Падтрымка Bitwarden не зможа вам дапамагчы, калі вы згубіце доступ да свайго ўліковага запіс. Мы рэкамендуем вам запісаць або раздрукаваць код аднаўлення і захоўваць яго ў надзейным месцы." + "message": "Уключэнне двухэтапнага ўваходу можа цалкам заблакіраваць доступ да ўліковага запісу Bitwarden. Код аднаўлення дае магчымасць атрымаць доступ да вашага ўліковага запісу ў выпадку, калі вы не можаце скарыстацца звычайным спосабам пастаўшчыка двухэтапнага ўваходу (напрыклад, вы згубілі сваю прыладу). Падтрымка Bitwarden не зможа вам дапамагчы, калі вы згубіце доступ да свайго ўліковага запісу. Мы рэкамендуем вам запісаць або раздрукаваць код аднаўлення і захоўваць яго ў надзейным месцы." }, "viewRecoveryCode": { "message": "Паглядзець код аднаўлення" @@ -1299,26 +1299,26 @@ "message": "Аднавіць доступ" }, "premium": { - "message": "Прэміяльны статус", + "message": "Прэміум", "description": "Premium Membership" }, "premiumMembership": { - "message": "Прэміяльны статус" + "message": "Прэміяльны ўдзельнік" }, "premiumRequired": { - "message": "Патрабуецца прэміяльны статус" + "message": "Патрабуецца прэміум" }, "premiumRequiredDesc": { "message": "Для выкарыстання гэтай функцыі патрабуецца прэміяльны статус." }, "youHavePremiumAccess": { - "message": "У вас прэміяльын статус" + "message": "У вас прэміяльны доступ" }, "alreadyPremiumFromOrg": { "message": "У вас ужо ёсць доступ да прэміяльных функцый, таму што вы з'яўляецеся ўдзельнікам арганізацыі, якая іх мае." }, "manage": { - "message": "Кіраваць" + "message": "Кіраванне" }, "disable": { "message": "Адключыць" @@ -1354,7 +1354,7 @@ "message": "Гэтыя праграмы з'яўляюцца рэкамендаванымі. Звярніце ўвагу, што іншыя праграмы таксама будуць працаваць." }, "twoStepAuthenticatorScanCode": { - "message": "Скануйце гэты QR-код з дапамогай праграмы аўтэнтыфікацыі" + "message": "Адскануйце гэты QR-код з дапамогай праграмы аўтэнтыфікацыі" }, "key": { "message": "Ключ" @@ -1387,13 +1387,13 @@ "message": "Захаваць форму." }, "twoFactorYubikeyWarning": { - "message": "З-за абмежаванняў платформы, YubiKey нельга выкарыстоўваць ва ўсіх праграмах Bitwarden. Калі YubiKey немагчыма выкарыстоўваць, вы павінны актываваць пастаўшчыка двухэтапнага ўваходу для атрымання доступу да свайго ўліковага запісу. Платформы, якія падтрымліваюцца:" + "message": "У сувязі з абмежаваннямі платформы, YubiKey нельга выкарыстоўваць ва ўсіх праграмах Bitwarden. Калі YubiKey немагчыма выкарыстоўваць, вы павінны актываваць пастаўшчыка двухэтапнага ўваходу для атрымання доступу да свайго ўліковага запісу. Платформы, якія падтрымліваюцца:" }, "twoFactorYubikeySupportUsb": { "message": "Вэб-сховішча, праграма для камп'ютара, інтэрфейс каманднага радка (CLI) і ўсе пашырэнні браўзера на прыладах з партом USB, якія сумяшчальныя з YubiKey." }, "twoFactorYubikeySupportMobile": { - "message": "Мабільныя праграмы на прыладах з NFC або портам USB, якія сумяшчальныя з YubiKey." + "message": "Мабільныя праграмы на прыладах з NFC або партом USB, якія сумяшчальны з YubiKey." }, "yubikeyX": { "message": "YubiKey $INDEX$", @@ -1489,13 +1489,13 @@ "message": "Захаваць форму." }, "twoFactorU2fWarning": { - "message": "З-за абмежаванняў платформы, FIDO U2F можна выкарыстоўваць не ва ўсіх праграмах Bitwarden. Калі FIDO U2F не атрымліваецца выкарыстоўваць, то вы павінны ўключыць другога пастаўшчыка двухэтапнага ўваходу, каб атрымаць доступ да вашага ўліковага запісу. Платформы, якія падтрымліваюцца:" + "message": "У сувязі з абмежаваннямі платформы, FIDO U2F можна выкарыстоўваць не ва ўсіх праграмах Bitwarden. Калі FIDO U2F не атрымліваецца выкарыстоўваць, то вы павінны ўключыць другога пастаўшчыка двухэтапнага ўваходу, каб атрымаць доступ да вашага ўліковага запісу. Платформы, якія падтрымліваюцца:" }, "twoFactorU2fSupportWeb": { "message": "Вэб-сховішча і пашырэнні браўзера на камп'ютары/ноўтбуку з браўзерам, які падтрымлівае U2F (Chrome, Opera, Vivaldi або Firefox з уключаным FIDO U2F)." }, "twoFactorU2fWaiting": { - "message": "Чаканне націску кнопкі на ключы бяспекі" + "message": "Чаканне націску кнопкі на вашым ключы бяспекі" }, "twoFactorU2fClickSave": { "message": "Націсніце кнопку \"Захаваць\" ніжэй, каб уключыць гэты ключ бяспекі для двухэтапнага ўваходу." @@ -1504,7 +1504,7 @@ "message": "Праблема чытання ключа бяспекі. Паспрабуйце яшчэ раз." }, "twoFactorWebAuthnWarning": { - "message": "З-за абмежавання платформы, WebAuthn немагчыма выкарыстоўваць ва ўсіх праграмах Bitwarden. Вам неабходна актываваць іншага пастаўшчыка двухэтапнага ўваходу, каб вы маглі атрымаць доступ да свайго ўліковага запісу, калі немагчыма скарыстацца WebAuthn. Платформы, які падтрымліваюцца:" + "message": "У сувязі з абмежаваннямі платформы, WebAuthn немагчыма выкарыстоўваць ва ўсіх праграмах Bitwarden. Вам неабходна актываваць іншага пастаўшчыка двухэтапнага ўваходу, каб вы маглі атрымаць доступ да свайго ўліковага запісу, калі немагчыма скарыстацца WebAuthn. Платформы, які падтрымліваюцца:" }, "twoFactorWebAuthnSupportWeb": { "message": "Вэб-сховішча і пашырэнні браўзера на камп'ютары/ноўтбуку з браўзерам, які падтрымлівае WebAuthn (Chrome, Opera, Vivaldi або Firefox з уключаным FIDO U2F)." @@ -1523,7 +1523,7 @@ "message": "Справаздачы" }, "reportsDesc": { - "message": "Выявіце і выпраўце недахопы ў бяспецы вашых уліковых запісах, націснуўшы на справаздачы ніжэй.", + "message": "Выявіце і выпраўце недахопы ў бяспецы вашых уліковых запісаў, націснуўшы на справаздачы ніжэй.", "description": "Vault Health Reports can be used to evaluate the security of your Bitwarden Personal or Organization Vault." }, "unsecuredWebsitesReport": { @@ -1536,7 +1536,7 @@ "message": "Знойдзены неабароненыя вэб-сайты" }, "unsecuredWebsitesFoundDesc": { - "message": "Мы знайшлі наступную колькасць з неабароненымі URI у вашым сховішчы: $COUNT$. Вам неабходна змяніць іх схему URI на https://, калі вэб-сайт дазваляе гэта зрабіць.", + "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з неабароненымі URI. Вам неабходна змяніць іх схему URI на https://, калі вэб-сайт дазваляе гэта зрабіць.", "placeholders": { "count": { "content": "$1", @@ -1557,7 +1557,7 @@ "message": "Знойдзены лагіны без 2ФА" }, "inactive2faFoundDesc": { - "message": "У сховішчы выяўлены вэб-сайты ($COUNT$ шт.), якія могуць быць не наладжаны для двухэтапнай аўтэнтыфікацыі (згодна з 2fa.directory). Для дадатковай абароны гэтых уліковых запісаў уключыце двухэтапную аўтэнтыфікацыю.", + "message": "У сховішчы ёсць вэб-сайты ($COUNT$ шт.), якія могуць быць не наладжаны для двухэтапнай аўтэнтыфікацыі (згодна з 2fa.directory). Для дадатковай абароны гэтых уліковых запісаў уключыце двухэтапную аўтэнтыфікацыю.", "placeholders": { "count": { "content": "$1", @@ -1605,16 +1605,16 @@ } }, "weakPasswordsReport": { - "message": "Слабыя паролі" + "message": "Ненадзейныя паролі" }, "weakPasswordsReportDesc": { - "message": "Слабыя паролі могуць быць лёгка падабраныя зламыснікамі. Змяніце гэтыя паролі на надзейныя з дапамогай генератара пароляў." + "message": "Ненадзейныя паролі могуць быць лёгка падабраныя зламыснікамі. Замяніце гэтыя паролі на надзейныя з дапамогай генератара пароляў." }, "weakPasswordsFound": { - "message": "Знойдзены слабыя паролі" + "message": "Знойдзены ненадзейныя паролі" }, "weakPasswordsFoundDesc": { - "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з ненадзейнымі паролямі. Вам неабходна змяніць іх на больш складаныя.", + "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з ненадзейнымі паролямі. Вам неабходна замяніць іх на больш надзейныя.", "placeholders": { "count": { "content": "$1", @@ -1635,7 +1635,7 @@ "message": "Знойдзены паўторныя паролі" }, "reusedPasswordsFoundDesc": { - "message": "У сховішчы знойдзены паўторныя паролі ($COUNT$ шт.). Змяніце іх на ўнікальныя.", + "message": "У сховішчы ёсць паўторныя паролі ($COUNT$ шт.). Вам неабходна згенерыраваць унікальныя паролі і замяніць іх.", "placeholders": { "count": { "content": "$1", @@ -1743,7 +1743,7 @@ "message": "Дададзеныя крэдыты з'явяцца на вашым рахунку пасля поўнай апрацоўкі плацяжу. Некаторыя спосабы аплаты адбываюцца з затрымкай і могуць заняць больш часу, чым астатнія." }, "makeSureEnoughCredit": { - "message": "Пераканайцеся, што на вашым рахунку дастаткова крэдытаў для ажыццяўлення гэтай пакупкі. Калі сродкаў на вашым рахунку не хапае, то для кампенсацыі нястачы будзе выкарыстаны ваш прадвызначаны метад аплаты. Вы можаце дадаць крэдыты на свой рахунак на старонцы аплаты." + "message": "Пераканайцеся, што на вашым рахунку дастаткова крэдытаў для ажыццяўлення гэтай пакупкі. Калі сродкаў на вашым рахунку не хапае, то для кампенсацыі нястачы будзе выкарыстаны ваш прадвызначаны спосаб аплаты. Вы можаце дадаць крэдыты на свой рахунак на старонцы аплаты." }, "creditAppliedDesc": { "message": "Крэдыт вашага рахунку можа выкарыстоўвацца для здзяйснення купляў. Любыя даступныя крэдыты будуць аўтаматычна ўжыты для рахункаў, якія згенерыраваны для гэтага ўліковага запісу." @@ -1753,7 +1753,7 @@ "description": "Another way of saying \"Get a premium membership\"" }, "premiumUpdated": { - "message": "Вы абнавіліся да прэміяльнага статусу." + "message": "Вы абнавіліся да прэміяльнай версіі." }, "premiumUpgradeUnlockFeatures": { "message": "Абнавіце свой уліковы запіс да платнай версіі і разблакіруйце некаторыя цудоўныя дадатковыя функцыі." @@ -1850,10 +1850,10 @@ "description": "Short abbreviation for 'month'" }, "paymentChargedAnnually": { - "message": "У вас адразу будзе спагнана плата згодна з абраным спосабам плацяжу, а інтэрвал такіх плацяжоў будзе ажыццяўляцца штогод. Вы можаце скасаваць іх у любы момант." + "message": "У вас адразу будзе спагнана плата згодна з выбраным спосабам аплаты, а інтэрвал такіх плацяжоў будзе ажыццяўляцца кожны год. Вы можаце скасаваць іх у любы момант." }, "paymentCharged": { - "message": "У вас адразу будзе спагнана плата згодна з абраным спосабам плацяжу, а інтэрвал такіх плацяжоў будзе ажыццяўляцца $INTERVAL$. Вы можаце скасаваць яго ў любы момант.", + "message": "У вас адразу будзе спагнана плата згодна з выбраным спосабам аплаты, а інтэрвал такіх плацяжоў будзе ажыццяўляцца кожны $INTERVAL$. Вы можаце скасаваць яго ў любы момант.", "placeholders": { "interval": { "content": "$1", @@ -1862,7 +1862,7 @@ } }, "paymentChargedWithTrial": { - "message": "У ваш тарыфны план уключаны выпрабавальны перыяд на 7 дзён. У вас не будзе спагнана плата згодна з абраным спосабам плацяжу пакуль выпрабавальны перыяд не скончыцца. Вы можаце скасаваць яго ў любы момант." + "message": "У ваш тарыфны план уключаны выпрабавальны перыяд на 7 дзён. У вас не будзе спагнана плата згодна з выбраным спосабам аплаты пакуль не завяршыцца выпрабавальны перыяд. Вы можаце скасаваць яго ў любы момант." }, "paymentInformation": { "message": "Плацежная інфармацыя" @@ -1871,7 +1871,7 @@ "message": "Плацежная інфармацыя" }, "billingTrialSubLabel": { - "message": "У вас не будзе спаганяцца плата абраным метадам плацяжу на працягу 7 дзён выпрабавальнага перыяду." + "message": "У вас не будзе спаганяцца плата згодна з выбраным спосабам аплаты на працягу 7 дзён выпрабавальнага перыяду." }, "creditCard": { "message": "Крэдытная картка" @@ -1928,7 +1928,7 @@ "message": "Ліцэнзія абноўлена" }, "manageSubscription": { - "message": "Кіраваць падпіскай" + "message": "Кіраванне падпіскай" }, "storage": { "message": "Сховішча" @@ -1956,7 +1956,7 @@ "message": "Спосаб аплаты" }, "noPaymentMethod": { - "message": "Файл не змяшчае метаду аплаты." + "message": "Файл не змяшчае спосабу аплаты." }, "addPaymentMethod": { "message": "Дадаць спосаб аплаты" @@ -1986,7 +1986,7 @@ "message": "Няма трансакцый." }, "chargeNoun": { - "message": "Спісанне", + "message": "Спагнанне", "description": "Noun. A charge from a payment method." }, "refundNoun": { @@ -2003,13 +2003,13 @@ } }, "gbStorageAdd": { - "message": "ГБ сховішча для дадавання" + "message": "ГБ сховішча для дабаўлення" }, "gbStorageRemove": { "message": "ГБ сховішча для выдалення" }, "storageAddNote": { - "message": "Дабаўленне сховішча прывядзе да карэкціроўкі ў вашым выніковым рахунку і адразу будзе спагнана аплата згодна з азначаным спосабам. Першы плацеж будзе прапарцыйны астачы бягучага плацежнага перыяду." + "message": "Дабаўленне сховішча прывядзе да карэкціроўкі ў вашым выніковым рахунку і адразу будзе спагнаны плацеж згодна з азначаным у файле спосабам аплаты. Першы плацеж будзе прапарцыйны астачы бягучага плацежнага перыяду." }, "storageRemoveNote": { "message": "Выдаленне сховішча прывядзе да карэкціроўкі вашага выніковага рахунку, які будзе прапарцыйна раздзелены ў выглядзе крэдытаў за наступны плацежны перыяд." @@ -2030,7 +2030,7 @@ "message": "Спосаб аплаты абноўлены." }, "purchasePremium": { - "message": "Купіць прэміяльны статус" + "message": "Купіць прэміум" }, "licenseFile": { "message": "Файл ліцэнзіі" @@ -2054,7 +2054,7 @@ "message": "Электронная пошта вашага ўліковага запісу павінна быць пацверджана." }, "newOrganizationDesc": { - "message": "Арганізацыі дазваляюць дзяліцца элементамі вашага сховішча з іншымі, а таксама кіраваць звязанымі карыстальнікамі для вызначанага аб'екта. Гэта можа быць сям'я, невялікая каманда або вялікая кампанія." + "message": "Арганізацыі дазваляюць абагуляць элементы вашага сховішча з іншымі, а таксама кіраваць звязанымі карыстальнікамі для вызначанага аб'екта. Гэта можа быць сям'я, невялікая каманда або вялікая кампанія." }, "generalInformation": { "message": "Агульная інфармацыя" @@ -2087,7 +2087,7 @@ "message": "# карыстальніцкіх месцаў" }, "userSeatsAdditionalDesc": { - "message": "Ваш тарыфны план мае наступную колькасць карыстальніцкіх месцаў: $BASE_SEATS$. Вы можаце дадаць дадатковую колькасць карыстальнікаў па кошце $SEAT_PRICE$ за карыстальніка ў месяц.", + "message": "Ваш тарыфны план мае наступную колькасць карыстальніцкіх месцаў: $BASE_SEATS$ шт. Вы можаце дадаць дадатковую колькасць карыстальнікаў па кошце $SEAT_PRICE$ за карыстальніка ў месяц.", "placeholders": { "base_seats": { "content": "$1", @@ -2203,7 +2203,7 @@ "message": "Лакальны хостынг (неабавязкова)" }, "usersGetPremium": { - "message": "Карыстальнікі атрымліваюць доступ да прэміяльных функцый" + "message": "Карыстальнікі атрымаюць доступ да прэміяльных функцый" }, "controlAccessWithGroups": { "message": "Кантроль доступу карыстальнікаў з дапамогай груп" @@ -2332,13 +2332,13 @@ "message": "Пасля таго, як удзельнік адкліканы, ён больш не зможа атрымаць доступ да даных арганізацыі. Для хуткага аднаўлення доступу ўдзельніка, перайдзіце ва ўкладку \"Адкліканыя\"." }, "removeUserConfirmationKeyConnector": { - "message": "Увага! Гэтаму карыстальніку патрабуецца Key Connector для таго, каб кіраваць шыфраваннем. Выдаленне гэтага карыстальніка з вашай арганізацыі канчаткова адключыць яго ўліковы запіс. Гэта дзеянне нельга будзе адрабіць. Вы сапраўды хочаце працягнуць?" + "message": "Папярэджанне! Гэтаму карыстальніку патрабуецца Key Connector для таго, каб кіраваць шыфраваннем. Выдаленне гэтага карыстальніка з вашай арганізацыі канчаткова адключыць яго ўліковы запіс. Гэта дзеянне нельга будзе адрабіць. Вы сапраўды хочаце працягнуць?" }, "externalId": { "message": "Знешні ідэнтыфікатар" }, "externalIdDesc": { - "message": "Знешні ідэнтыфікатара можа быць выкарыстаны ў якасці спасылкі для сувязі гэтага рэсурсу са знешняй сістэмай, такой як каталог карыстальніка." + "message": "Знешні ідэнтыфікатар можа быць выкарыстаны ў якасці спасылкі для сувязі гэтага рэсурсу са знешняй сістэмай, такой як каталог карыстальніка." }, "accessControl": { "message": "Кантроль доступу" @@ -2371,7 +2371,7 @@ "message": "Запрасіць карыстальніка" }, "inviteUserDesc": { - "message": "Запрасіце новага карыстальніка ў вашу арганізацыю ўвёўшы яго электронную пошту ўліковага запісу Bitwarden. Калі ў яго пакуль няма ўліковага запісу Bitwarden, яму будзе прапанавана стварыць яго." + "message": "Запрасіць новага карыстальніка ў вашу арганізацыю, увёўшы яго электронную пошту ўліковага запісу Bitwarden. Калі ён не мае ўліковага запісу, то ён атрымае запыт на яго стварэнне." }, "inviteMultipleEmailDesc": { "message": "Вы можаце запрасіць да $COUNT$ карыстальнікаў за раз, падзяляючы адрасы электроннай пошты ў спісе коскамі.", @@ -2467,7 +2467,7 @@ "message": "Вэб-сховішча" }, "loggedIn": { - "message": "Выкананы ўваход." + "message": "Вы ўвайшлі." }, "changedPassword": { "message": "Пароль уліковага запісу зменены." @@ -2506,7 +2506,7 @@ } }, "editedItemId": { - "message": "Адрэдагаваны элемент $ID$.", + "message": "Элемент $ID$ адрэдагаваны.", "placeholders": { "id": { "content": "$1", @@ -2614,7 +2614,7 @@ } }, "editedCollectionId": { - "message": "Адрэдагавана калекцыя $ID$.", + "message": "Калекцыя $ID$ адрэдагавана.", "placeholders": { "id": { "content": "$1", @@ -2632,7 +2632,7 @@ } }, "editedPolicyId": { - "message": "Адрэдагаваная палітыка $ID$.", + "message": "Палітыка $ID$ адрэдагавана.", "placeholders": { "id": { "content": "$1", @@ -2650,7 +2650,7 @@ } }, "editedGroupId": { - "message": "Адрэдагавана група $ID$.", + "message": "Група $ID$ адрэдагавана.", "placeholders": { "id": { "content": "$1", @@ -2731,7 +2731,7 @@ } }, "editedCollectionsForItem": { - "message": "Адрэдагаваныя калекцыі для элемента $ID$.", + "message": "Калекцыі для элемента $ID$ адрэдагаваны.", "placeholders": { "id": { "content": "$1", @@ -2758,7 +2758,7 @@ } }, "editedUserId": { - "message": "Адрэдагаваны карыстальнік $ID$.", + "message": "Карыстальнік $ID$ адрэдагаваны.", "placeholders": { "id": { "content": "$1", @@ -2767,7 +2767,7 @@ } }, "editedGroupsForUser": { - "message": "Адрэдагаваныя групы для карыстальніка $ID$.", + "message": "Групы для карыстальніка $ID$ адрэдагаваны.", "placeholders": { "id": { "content": "$1", @@ -2776,7 +2776,7 @@ } }, "unlinkedSsoUser": { - "message": "Нязвязныя SSO для карыстальніка $ID$.", + "message": "Адлучаныя SSO для карыстальніка $ID$.", "placeholders": { "id": { "content": "$1", @@ -2881,7 +2881,7 @@ "message": "Пацвердзіць карыстальнікаў" }, "usersNeedConfirmed": { - "message": "У вас ёсць карыстальнікі, які прынялі запрашэнне, але патрабуюць пацвярджэння. Гэтыя карыстальнікі не будуць мець доступу да арганізацыі, пакуль не будуць пацверджаны." + "message": "У вас ёсць карыстальнікі, якія прынялі запрашэнне, але патрабуюць пацвярджэння. Гэтыя карыстальнікі не будуць мець доступу да арганізацыі, пакуль не будуць пацверджаны." }, "startDate": { "message": "Дата пачатку" @@ -2893,7 +2893,7 @@ "message": "Праверыць пошту" }, "verifyEmailDesc": { - "message": "Параверце ваш адрас электроннай пошты для разблакіроўкі доступу да ўсіх функцый." + "message": "Праверце ваш адрас электроннай пошты для разблакіроўкі доступу да ўсіх функцый." }, "verifyEmailFirst": { "message": "Спачатку неабходна праверыць адрас электроннай пошты вашага ўліковага запісу." @@ -2929,7 +2929,7 @@ "message": "Запрашэнне прынята" }, "inviteAcceptedDesc": { - "message": "Вы можаце атрымаць доступ да гэтага арганізацыі адразу пасля таго, як будзе пацверджаны ваш удзел. Мы апавясцім вас, калі гэта адбудзецца." + "message": "Вы можаце атрымаць доступ да гэтай арганізацыі адразу пасля таго, як будзе пацверджаны ваш удзел. Мы апавясцім вас, калі гэта адбудзецца." }, "inviteAcceptFailed": { "message": "Немагчыма прыняць запрашэнне. Папрасіце адміністратара арганізацыі адправіць яго яшчэ раз." @@ -3014,7 +3014,7 @@ "description": "A billing plan/package. For example: families, teams, enterprise, etc." }, "changeBillingPlan": { - "message": "Палепшыць план", + "message": "Палепшыць тарыфны план", "description": "A billing plan/package. For example: families, teams, enterprise, etc." }, "changeBillingPlanUpgrade": { @@ -3041,10 +3041,10 @@ "message": "Праверыць банкаўскі рахунак" }, "verifyBankAccountDesc": { - "message": "Мы зрабілі два сімвалічныя ўклады на ваш банкаўскі рахунак (ён адлюструецца ў спісе на працягу 1-2 працоўных дзён). Увядзіце гэту суму для праверкі банкаўскага рахунку." + "message": "Мы зрабілі два сімвалічныя ўклады на ваш банкаўскі рахунак (ён адлюструецца ў спісе на працягу некалькіх наступных працоўных дзён). Увядзіце гэту суму для праверкі банкаўскага рахунку." }, "verifyBankAccountInitialDesc": { - "message": "Плацеж праз банкаўскі рахунак даступны толькі кліентам, якія пражываюць у ЗША. Вам неабходна будзе пацвердзіць свой банкаўскі рахунак. Мы зробім два сімвалічныя ўклады на працягу наступных 1-2 дзён. Увядзіце гэтыя сумы на старонцы аплаты арганізацыі для праверкі банкаўскага рахунку." + "message": "Плацеж праз банкаўскі рахунак даступны толькі кліентам, якія пражываюць у ЗША. Вам неабходна будзе пацвердзіць свой банкаўскі рахунак. Мы зробім два сімвалічныя ўклады на працягу наступных некалькіх дзён. Увядзіце гэтыя сумы на старонцы аплаты арганізацыі для праверкі банкаўскага рахунку." }, "verifyBankAccountFailureWarning": { "message": "Невыкананне праверкі банкаўскага рахунку прывядзе да збою аплаты і адключэння падпіскі." @@ -3109,7 +3109,7 @@ "message": "Карэкціроўка вашай падпіскі прывядзе да прапарцыйнага змянення ў вашым выніковым рахунку. Калі колькасць карыстальнікаў перавысіць колькасць месцаў у вашай падпісцы, вы адразу атрымаеце прапарцыйную плату за дадатковых карыстальнікаў." }, "subscriptionUserSeats": { - "message": "Агульная колькасць карыстальнікаў для вашай падпіскі: $COUNT$.", + "message": "Агульная колькасць карыстальнікаў для вашай падпіскі: $COUNT$ шт.", "placeholders": { "count": { "content": "$1", @@ -3163,7 +3163,7 @@ } }, "subscriptionSponsoredFamiliesPlan": { - "message": "Колькасць карыстальнікаў для вашай падпіскі: $COUNT$. Ваш тарыфны план мае спонсарскую падтрымку і аплачваецца знешняй арганізацыяй.", + "message": "Колькасць карыстальнікаў для вашай падпіскі: $COUNT$ шт. Ваш тарыфны план мае спонсарскую падтрымку і аплачваецца знешняй арганізацыяй.", "placeholders": { "count": { "content": "$1", @@ -3187,7 +3187,7 @@ "message": "Месцаў для выдалення" }, "seatsAddNote": { - "message": "Дадаванне карыстальніцкіх месцаў прывядзе да карэкціроўкі ў вашым выніковым рахунку і адразу будзе спагнана аплата згодна з азначаным спосабам. Першы плацеж будзе прапарцыйны астачы бягучага плацежнага перыяду." + "message": "Дабаўленне карыстальніцкіх месцаў прывядзе да карэкціроўкі ў вашым выніковым рахунку і адразу будзе спагнаны плацеж згодна з азначаным у файле спосабам аплаты. Першы плацеж будзе прапарцыйны астачы бягучага плацежнага перыяду." }, "seatsRemoveNote": { "message": "Выдаленне карыстальніцкіх месцаў прывядзе да карэкціроўкі вашага выніковага рахунку, які будзе прапарцыйна раздзелены ў выглядзе крэдытаў за наступны плацежны перыяд." @@ -3214,7 +3214,7 @@ "message": "Зараз вы выкарыстоўваеце састарэлую схему шыфравання." }, "updateEncryptionKeyDesc": { - "message": "Мы перайшлі на больш складаныя ключы шыфравання, якія забяспечваюць лепшую бяспеку і доступ да самых новых функцый. Абнаўленне вашы ключоў шыфравання адбываецца хутка і лёгка. Проста ўвядзіце свой асноўны пароль знізу. Гэта абнаўленне ўрэшце стане абавязковым." + "message": "Мы перайшлі на больш складаныя ключы шыфравання, якія забяспечваюць лепшую бяспеку і доступ да самых новых функцый. Абнаўленне вашых ключоў шыфравання адбудзецца хутка і лёгка. Проста ўвядзіце свой асноўны пароль знізу. Гэта абнаўленне ўрэшце стане абавязковым." }, "updateEncryptionKeyWarning": { "message": "Пасля абнаўлення вашага ключа шыфравання вам неабходна выйсці з сістэмы, а потым выканаць паўторны ўваход ва ўсе праграмы Bitwarden, якія вы зараз выкарыстоўваеце (напрыклад, мабільныя праграмы або пашырэнні для браўзераў). Збой пры выхадзе і паўторным уваходзе (пры гэтым спампоўваецца ваш новы ключ шыфравання) можа стаць прычынай пашкоджання даных. Мы паспрабуем аўтаматычна ажыццявіць завяршэнне ўсіх вашых сеансаў, але гэта можа адбывацца з затрымкай." @@ -3247,7 +3247,7 @@ "message": "Вернута" }, "nothingSelected": { - "message": "Вы нічога не выбралі." + "message": "Вы пакуль нічога не выбралі." }, "acceptPolicies": { "message": "Ставячы гэты сцяжок, вы пагаджаецеся з наступным:" @@ -3321,7 +3321,7 @@ "message": "Хто валодае гэтым элементам?" }, "strong": { - "message": "Моцны", + "message": "Надзейны", "description": "ex. A strong password. Scale: Very Weak -> Weak -> Good -> Strong" }, "good": { @@ -3329,15 +3329,15 @@ "description": "ex. A good password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weak": { - "message": "Слабы", + "message": "Ненадзейны", "description": "ex. A weak password. Scale: Very Weak -> Weak -> Good -> Strong" }, "veryWeak": { - "message": "Вельмі слабы", + "message": "Вельмі ненадзейны", "description": "ex. A very weak password. Scale: Very Weak -> Weak -> Good -> Strong" }, "weakMasterPassword": { - "message": "Слабы асноўны пароль" + "message": "Ненадзейны асноўны пароль" }, "weakMasterPasswordDesc": { "message": "Асноўны пароль, які вы выбралі з'яўляецца ненадзейным. Для належнай абароны ўліковага запісу Bitwarden, вы павінны выкарыстоўваць надзейны асноўны пароль (або парольную фразу). Вы ўпэўнены, што хочаце выкарыстоўваць гэты асноўны пароль?" @@ -3384,7 +3384,7 @@ "message": "Ключ API" }, "apiKeyDesc": { - "message": "Ваш ключ API можа быць выкарыстаны для аўтэнтыфікацыі публічнага API праграмы." + "message": "Ваш ключ API можа быць выкарыстаны для аўтэнтыфікацыі публічнага API праграмы Bitwarden." }, "apiKeyRotateDesc": { "message": "Змяненне ключа API анулюе папярэдні ключ. Вы можаце змяніць ваш ключ API, калі ёсць любыя сумневы адносна бяспекі бягучага ключа." @@ -3418,7 +3418,7 @@ "message": "Купля ў праграме" }, "cannotPerformInAppPurchase": { - "message": "Вы не можаце выканаць гэта дзеянне падчас выкарыстання аплаты ў праграме." + "message": "Вы не можаце выканаць гэта дзеянне падчас выкарыстання спосабу аплаты ў праграме." }, "manageSubscriptionFromStore": { "message": "Вы павінны кіраваць сваёй падпіскай з той крамы, у якой вы ажыццяўлялі куплю праграмы." @@ -3433,16 +3433,16 @@ "message": "Патрабуецца асноўны пароль" }, "masterPassPolicyDesc": { - "message": "Задайце мінімальныя патрабаванні да надзейнасці асноўнага пароля." + "message": "Прызначце мінімальныя патрабаванні да надзейнасці асноўнага пароля." }, "twoStepLoginPolicyTitle": { "message": "Патрабуецца двухэтапны ўваход" }, "twoStepLoginPolicyDesc": { - "message": "Патрабаваць удзельнікаў наладу двуэтапнага ўваходу." + "message": "Патрабаваць ад удзельнікаў наладу двуэтапнага ўваходу." }, "twoStepLoginPolicyWarning": { - "message": "Удзельнікі арганізацыі, якія не з'яўляюцца ўладальнікамі або адміністратарамі і якія не ўключылі двухэтапны ўваход у сваіх уліковых запісаць будуць выдалены з арганізацыі і атрымаюць адпаведнае апавяшчэнне па электроннай пошце." + "message": "Удзельнікі арганізацыі, якія не з'яўляюцца ўладальнікамі або адміністратарамі і якія не ўключылі двухэтапны ўваход у сваіх уліковых запісах будуць выдалены з арганізацыі і атрымаюць адпаведнае апавяшчэнне па электроннай пошце." }, "twoStepLoginPolicyUserWarning": { "message": "Вы з'яўляецеся ўдзельнікам арганізацыі, якая патрабуе выкарыстанне двухэтапнага ўваходу для вашага ўліковага запісу. Калі вы адключыце ўсіх пастаўшчыкоў двухэтапнага ўваходу, вы будзеце аўтаматычна выдалены з гэтай арганізацыі." @@ -3451,7 +3451,7 @@ "message": "Прызначыць патрабаванні для генератара пароляў." }, "passwordGeneratorPolicyInEffect": { - "message": "На налады генератара ўплываюць адна або некалькі палітык арганізацый." + "message": "Адна або больш палітык арганізацыі ўплывае на налады генератара." }, "masterPasswordPolicyInEffect": { "message": "Згодна з адной або некалькімі палітыкамі арганізацыі асноўны пароль павінен адпавядаць наступным патрабаванням:" @@ -3505,7 +3505,7 @@ "message": "Параметры карыстальніка" }, "vaultTimeoutAction": { - "message": "Дзеянне пасля заканчэння часу сховішча" + "message": "Дзеянне пасля заканчэння часу чакання сховішча" }, "vaultTimeoutActionLockDesc": { "message": "Для атрымання доступу да заблакіраванага сховішча, вам неабходна ўвесці асноўны пароль або скарыстацца іншым метадам разблакіроўкі." @@ -3525,7 +3525,7 @@ "message": "Пошук у сметніцы" }, "permanentlyDelete": { - "message": "Выдаліць канчаткова" + "message": "Выдаліць назаўсёды" }, "permanentlyDeleteSelected": { "message": "Назаўсёды выдаліць выбраныя" @@ -3543,7 +3543,7 @@ "message": "Элементы выдалены назаўсёды" }, "permanentlyDeleteSelectedItemsDesc": { - "message": "Вы выбралі наступную колькасць запісаў для выдалення: $COUNT$ шт. Вы ўпэўнены, што хочаце назаўсёды выдаліць гэтыя элементы?", + "message": "Вы выбралі наступную колькасць элементаў для выдалення: $COUNT$ шт. Вы ўпэўнены, што хочаце назаўсёды выдаліць іх?", "placeholders": { "count": { "content": "$1", @@ -3582,7 +3582,7 @@ "message": "Аднавіць элементы" }, "restoreSelectedItemsDesc": { - "message": "Вы выбралі наступную колькасць запісаў для аднаўлення: $COUNT$. Вы ўпэўнены, што хочаце аднавіць гэтыя элементы?", + "message": "Вы выбралі наступную колькасць элементаў для аднаўлення: $COUNT$. Вы ўпэўнены, што хочаце аднавіць іх?", "placeholders": { "count": { "content": "$1", @@ -3600,7 +3600,7 @@ } }, "vaultTimeoutLogOutConfirmation": { - "message": "Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыі праз інтэрнэт пасля завяршэння часу чакання. Вы ўпэўнены, што хочаце выкарыстоўваць гэты параметр?" + "message": "Выхад з сістэмы скасуе ўсе магчымасці доступу да сховішча і запатрабуе аўтэнтыфікацыю праз інтэрнэт пасля завяршэння часу чакання. Вы ўпэўнены, што хочаце выкарыстоўваць гэты параметр?" }, "vaultTimeoutLogOutConfirmationTitle": { "message": "Пацвярджэнне дзеяння часу чакання" @@ -3621,7 +3621,7 @@ "message": "Падатковая інфармацыя абноўлена." }, "setMasterPassword": { - "message": "Задаць асноўны пароль" + "message": "Прызначыць асноўны пароль" }, "ssoCompleteRegistration": { "message": "Для завяршэння працэсу ўваходу з дапамогай SSO, прызначце асноўны пароль для доступу да вашага сховішча і яго абароны." @@ -3636,7 +3636,7 @@ "message": "Уваходзьце з выкарыстаннем партала адзінага ўваходу вашай арганізацыі. Калі ласка, увядзіце ідэнтыфікатар вашай арганізацыі для пачатку працы." }, "enterpriseSingleSignOn": { - "message": "Адзіны карпаратыўны ўваход (SSO)" + "message": "Адзіны ўваход прадпрыемства (SSO)" }, "ssoHandOff": { "message": "Цяпер вы можаце закрыць гэту ўкладку і працягнуць у пашырэнні." @@ -3663,10 +3663,10 @@ "message": "Вы ўпэўнены, што хочаце адлучыць SSO для гэтай арганізацыі?" }, "linkSso": { - "message": "Падлучыць SSO" + "message": "Звязаць з SSO" }, "singleOrg": { - "message": "Адна арганізацыя" + "message": "Адзіная арганізацыя" }, "singleOrgDesc": { "message": "Забараніць удзельнікам далучацца да іншых арганізацый." @@ -3681,13 +3681,13 @@ "message": "Патрабаваць аўтэнтыфікацыю праз адзіны ўваход (SSO)" }, "requireSsoPolicyDesc": { - "message": "Патрабаваць ад удзельнікаў уваходзіць праз адзіны карпаратыўны ўваход (SSO)." + "message": "Патрабаваць ад удзельнікаў уваходзіць праз адзіны ўваход прадпрыемства (SSO)." }, "prerequisite": { "message": "Перадумова" }, "requireSsoPolicyReq": { - "message": "Перад актывацыяй гэтай палітыкі неабходна ўключыць палітыку адзінага ўваходу (SSO)." + "message": "Перад актывацыяй гэтай палітыкі неабходна ўключыць палітыку адзінага ўваходу (SSO) прадпрыемства." }, "requireSsoPolicyReqError": { "message": "Палітыка адзінай арганізацыі не ўключана." @@ -3714,7 +3714,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "editedSend": { - "message": "Адрэдагаваны Send", + "message": "Send адрэдагаваны", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletedSend": { @@ -3730,7 +3730,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "whatTypeOfSend": { - "message": "Які гэта тып Send?", + "message": "Які гэта тып Send'a?", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "deletionDate": { @@ -3772,7 +3772,7 @@ "message": "Адклікана" }, "sendLink": { - "message": "Адправіць спасылку", + "message": "Спасылка на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "copySendLink": { @@ -3809,7 +3809,7 @@ "message": "Пратэрмінавана" }, "searchSends": { - "message": "Шукаць адпраўленні", + "message": "Пошук у Send'ах", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPassword": { @@ -3817,7 +3817,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "Не ведаеце пароль? Спытайце ў адпраўніка пароль, які неабходны для доступу да гэтага Send'а.", + "message": "Не ведаеце пароль? Спытайце ў адпраўніка пароль, які неабходны для доступу да гэтага Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { @@ -3836,14 +3836,14 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "noSendsInList": { - "message": "У спісе адсутнічае Send.", + "message": "У спісе адсутнічаюць Send'ы.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "emergencyAccess": { "message": "Экстранны доступ" }, "emergencyAccessDesc": { - "message": "Забяспечце экстранны доступ давераным кантактам. Давераныя кантакты могуць атрымаць доступ для прагляду або перадачы валодання вашым уліковы запісам у экстранных сітуацыях. Наведайце нашу старонку дапамогі для падрабязнага азнаямлення, як працуе супольны доступ нулявога ўзроўню." + "message": "Дайце экстранны доступ давераным кантактам. Давераныя кантакты могуць атрымаць доступ для прагляду або перадачы валодання вашым уліковы запісам у экстранных сітуацыях. Наведайце нашу старонку дапамогі для падрабязнага азнаямлення, як працуе супольны доступ нулявога ўзроўню." }, "emergencyAccessOwnerWarning": { "message": "Вы ўладальнік адной або некалькіх арганізацый. Калі перадаць доступ валодання экстранным кантактам, то ён будзе мець усе дазволы ўладальніка пасля перадачы." @@ -3861,7 +3861,7 @@ "message": "Прызначаны ў якасці экстраннага кантакту" }, "noGrantedAccess": { - "message": "Вы нікога пакуль не прызначылі ў якасці экстраннага кантакту." + "message": "У вас пакуль няма экстранных кантактаў." }, "inviteEmergencyContact": { "message": "Запрасіць экстранны кантакт" @@ -3939,7 +3939,7 @@ } }, "requestSent": { - "message": "Экстранны доступ запрошаны для $USER$. Вы апавясцім вас па электроннай пошце, калі можна будзе працягнуць.", + "message": "Экстранны доступ запрошаны для $USER$. Мы апавясцім вас па электроннай пошце, калі можна будзе працягнуць.", "placeholders": { "user": { "content": "$1", @@ -3973,7 +3973,7 @@ "message": "Экстранны доступ адхілены" }, "passwordResetFor": { - "message": "Пароль скінуты для $USER$. Цяпер вы ўвайсці з дапамогай новага пароля.", + "message": "Пароль скінуты для $USER$. Цяпер вы можаце ўвайсці з дапамогай новага пароля.", "placeholders": { "user": { "content": "$1", @@ -3991,13 +3991,13 @@ "message": "На ўладальнікаў арганізацыі і адміністратараў гэта палітыка не аказвае ўплыву." }, "personalOwnershipSubmitError": { - "message": "У адпаведнасці з карпаратыўнай палітыкай вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." + "message": "У адпаведнасці з палітыкай прадпрыемства вам забаронена захоўваць элементы ў асабістым сховішчы. Змяніце параметры ўласнасці на арганізацыю і выберыце з даступных калекцый." }, "disableSend": { "message": "Выдаліць Send" }, "disableSendPolicyDesc": { - "message": "Не дазваляць удзельнікам ствараць або рэдагаваць Send'ы.", + "message": "Не дазваляць удзельнікам ствараць або рэдагаваць Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "disableSendExemption": { @@ -4008,11 +4008,11 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendDisabledWarning": { - "message": "У адпаведнасці з карпаратыўнай палітыкай, вы можаце выдаліць толькі бягучы Send.", + "message": "У адпаведнасці з палітыкай прадпрыемства, вы можаце выдаліць толькі бягучы Send.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptions": { - "message": "Параметры адпраўкі", + "message": "Параметры Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptionsPolicyDesc": { @@ -4070,7 +4070,7 @@ "message": "У вас недастаткова правоў для выканання гэтага дзеяння." }, "manageAllCollections": { - "message": "Кіраваць усімі калекцыямі" + "message": "Кіраванне ўсімі калекцыямі" }, "createNewCollections": { "message": "Старыць новыя калекцыі" @@ -4082,7 +4082,7 @@ "message": "Выдаліць любую калекцыю" }, "manageAssignedCollections": { - "message": "Кіраваць прызначанымі калекцыямі" + "message": "Кіраванне прызначанымі калекцыямі" }, "editAssignedCollections": { "message": "Рэдагаваць прызначаныя калекцыі" @@ -4141,7 +4141,7 @@ "message": "Скапіяваць спасылку ў буфер абмену пасля захавання, каб абагуліць гэты Send." }, "sendLinkLabel": { - "message": "Адправіць спасылку", + "message": "Спасылка на Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "send": { @@ -4250,7 +4250,7 @@ "message": "Адмова паспяхова ўжыта!" }, "eventEnrollPasswordReset": { - "message": "Карыстальнік $ID$ зарэгістраваўся, каб атрымаць дапамогу ў скіданні пароля.", + "message": "Карыстальнік $ID$ падаў запыт, каб атрымаць дапамогу ў скіданні пароля.", "placeholders": { "id": { "content": "$1", @@ -4340,7 +4340,7 @@ "message": "Аўтаматычна рэгістраваць новых карыстальнікаў" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "Гэта арганізацыя мае карпаратыўную палітыку, якая аўтаматычна зарэгіструе ваша скіданне пароля. Рэгістрацыя дазволіць адміністратарам арганізацыі змяняць ваш асноўны пароль." + "message": "Гэта арганізацыя мае палітыку прадпрыемства, якая аўтаматычна зарэгіструе ваша скіданне пароля. Рэгістрацыя дазволіць адміністратарам арганізацыі змяняць ваш асноўны пароль." }, "resetPasswordOrgKeysError": { "message": "Пусты адказ на ключы арганізацыі" @@ -4352,7 +4352,7 @@ "message": "Элементы, якія знаходзяцца ў сметніцы больш чым 30 дзён будуць выдаляцца аўтаматычна." }, "trashCleanupWarningSelfHosted": { - "message": "Элементы, якія будуць знаходзіцца ў сметніцы пэўны час будуць выдаляцца аўтаматычна." + "message": "Элементы, якія знаходзіцца ў сметніцы пэўны час будуць выдаляцца аўтаматычна." }, "passwordPrompt": { "message": "Паўторны запыт асноўнага пароля" @@ -4373,10 +4373,10 @@ "message": "Вы ўпэўнены, што хочаце выдаліць наступных карыстальнікаў? Для завяршэння працэсу можа спатрэбіцца некалькі секунд і гэта дзеянне нельга перарваць або скасаваць." }, "removeOrgUsersConfirmation": { - "message": "Пасля выдалення ўдзельніка(-ў) яны згубяць доступ да даных арганізацыі і гэта дзеянне з'яўляецца незваротным. Для паўторнага дабаўлення ўдзельніка ў арганізацыю, яго неабходна будзе зноў запрасіць туды. Працэс можа заняць некалькі секунд і яго немагчыма перарваць або скасаваць." + "message": "Пасля выдалення ўдзельніка(-ў) ён згубіць доступ да даных арганізацыі і гэта дзеянне з'яўляецца незваротным. Для паўторнага дабаўлення ўдзельніка ў арганізацыю, яго неабходна будзе зноў запрасіць туды. Працэс можа заняць некалькі секунд і яго немагчыма перарваць або скасаваць." }, "revokeUsersWarning": { - "message": "Калі ўдзельнік(-і) адкліканы, яны больш не змогуць атрымаць доступ да даных арганізацыі. Для хуткага аднаўлення доступу ўдзельніка, перайдзіце ва ўкладку \"Адкліканыя\". Для завяршэння працэсу можа спатрэбіцца некалькі секунд і гэта дзеянне не можа быць перарвана або скасавана." + "message": "Калі ўдзельнік(-і) адкліканы, ён больш не зможа атрымаць доступ да даных арганізацыі. Для хуткага аднаўлення доступу ўдзельніка, перайдзіце ва ўкладку \"Адкліканыя\". Для завяршэння працэсу можа спатрэбіцца некалькі секунд і гэта дзеянне не можа быць перарвана або скасавана." }, "theme": { "message": "Тэма" @@ -4439,7 +4439,7 @@ "message": "Налады пастаўшчыка" }, "setupProviderLoginDesc": { - "message": "Вы былі запрошаны для наладжвання новага пастаўшчыка. Для працягу вам неабходна ўвайсці або стврыць новы ўліковы запіс Bitwarden." + "message": "Вы былі запрошаны для наладжвання новага пастаўшчыка. Для працягу вам неабходна ўвайсці або стварыць новы ўліковы запіс Bitwarden." }, "setupProviderDesc": { "message": "Калі ласка, увядзіце дэталі ніжэй для завяршэння наладжвання правайдара. Вы можаце звязацца са службай падтрымкі, калі ў вас узніклі пытанні." @@ -4472,7 +4472,7 @@ "message": "Далучыцца да пастаўшчыка" }, "joinProviderDesc": { - "message": "Вас запрасілі далучыцца да азначанага вышэй пастаўшчыка. Для таго, каб пацвердзіць запрашэнне, вам неабходна ўвайсці ў ваш уліковы запіс Bitwarden або стварыць яго." + "message": "Вас запрасілі далучыцца да азначанага вышэй пастаўшчыка. Для таго, каб пацвердзіць запрашэнне, вам неабходна ўвайсці або стварыць новы ўліковы запіс Bitwarden." }, "providerInviteAcceptFailed": { "message": "Немагчыма прыняць запрашэнне. Папрасіце адміністратара пастаўшчыка адправіць яго яшчэ раз." @@ -4481,7 +4481,7 @@ "message": "Вы можаце атрымаць доступ да гэтага пастаўшчыка адразу пасля таго, як будзе пацверджаны ваш удзел. Мы апавясцім вас, калі гэта адбудзецца." }, "providerUsersNeedConfirmed": { - "message": "У вас ёсць карыстальнікі, якія прынялі запрашэнне, але яшчэ не былі пацверджаны. Яны не будуць мець доступу да пастаўшчыка пакуль не будзе пацверджаны іх удзел." + "message": "У вас ёсць карыстальнікі, якія прынялі запрашэнне, але яшчэ не былі пацверджаны. Яны не будуць мець доступу да пастаўшчыка пакуль не будуць пацверджаны." }, "provider": { "message": "Пастаўшчык" @@ -4560,10 +4560,10 @@ "message": "Абнавіць асноўны пароль" }, "updateMasterPasswordWarning": { - "message": "Ваш асноўны пароль быў нядаўна зменены адміністратарам вашай арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць асноўны пароль. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." + "message": "Ваш асноўны пароль быў нядаўна зменены адміністратарам вашай арганізацыі. Для атрымання доступу да сховішча, вы павінны абнавіць яго. Працягваючы, вы выйдзіце з бягучага сеанса і вам неабходна будзе ўвайсці паўторна. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу адной гадзіны." }, "masterPasswordInvalidWarning": { - "message": "Ваш асноўны пароль не адпавядае патрабаванням гэтай арганізацыі. Каб далучыцца да арганізацыі, вам неабходна абнавіць свой асноўны пароль зараз. Працягнуўшы, вы выйдзіце з бягучага сеанса і вам спатрэбіцца ўвайсці зноў. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу гадзіны." + "message": "Ваш асноўны пароль не адпавядае патрабаванням гэтай арганізацыі. Каб далучыцца да арганізацыі, вам неабходна абнавіць яго зараз. Працягнуўшы, вы выйдзіце з бягучага сеанса і вам спатрэбіцца ўвайсці зноў. Сеансы на іншых прыладах могуць заставацца актыўнымі на працягу гадзіны." }, "maximumVaultTimeout": { "message": "Час чакання сховішча" @@ -4584,7 +4584,7 @@ "message": "Хвіліны" }, "vaultTimeoutPolicyInEffect": { - "message": "Палітыка вашай арганізацыі ўплывае на час чакання сховішча. Максімальны дазволены час чакання сховішча $HOURS$ гадз. і $MINUTES$ хв.", + "message": "Палітыка вашай арганізацыі ўплывае на час чакання сховішча. Максімальны дазволены час чакання сховішча складае $HOURS$ гадз. і $MINUTES$ хв.", "placeholders": { "hours": { "content": "$1", @@ -4756,7 +4756,7 @@ "message": "Спасылка больш не дзейнічае. Калі ласка, папрасіце спонсара паўторна адправіць прапанову." }, "reclaimedFreePlan": { - "message": "Бясплатны план адноўлены" + "message": "Бясплатны тарыфны план адноўлены" }, "redeem": { "message": "Актываваць" @@ -4780,7 +4780,7 @@ "message": "Вам прапанаваны бясплатны тарыфны план Bitwarden Families ад арганізацыі. Для працягу, вам неабходна ўвайсці ва ўліковы запіс з якога вы атрымалі прапанову." }, "sponsoredFamiliesAcceptFailed": { - "message": "Немагчыма праняць прапанову. Калі ласка, адпраўце прапанову на электронную пошту паўторна са свайго карпаратыўнага ўліковага запісу." + "message": "Немагчыма праняць прапанову. Калі ласка, адпраўце прапанову на электронную пошту паўторна са свайго ўліковага запісу прадпрыемства." }, "sponsoredFamiliesAcceptFailedShort": { "message": "Немагчыма праняць прапанову. $DESCRIPTION$", @@ -4813,7 +4813,7 @@ } }, "resendEmailLabel": { - "message": "Паўторна адправіць ліст пра спансаванне карыстальніку $NAME$", + "message": "Паўторна адправіць ліст пра спансіраванне карыстальніку $NAME$", "placeholders": { "name": { "content": "$1", @@ -4822,7 +4822,7 @@ } }, "freeFamiliesPlan": { - "message": "Бясплатны сямейны план" + "message": "Бясплатны тарыфны план Bitwarden Families" }, "redeemNow": { "message": "Актываваць зараз" @@ -4831,22 +4831,22 @@ "message": "Атрымальнік" }, "removeSponsorship": { - "message": "Выдаліць спансаванне" + "message": "Выдаліць спансіраванне" }, "removeSponsorshipConfirmation": { "message": "Пасля выдалення спонсарства, вы будзеце адказваць за гэту падпіску і звязаныя рахункі. Вы ўпэўнены, што хочаце працягнуць?" }, "sponsorshipCreated": { - "message": "Спансаванне створана" + "message": "Спансіраванне створана" }, "emailSent": { "message": "Ліст адпраўлены" }, "revokeSponsorshipConfirmation": { - "message": "Пасля выдалення гэтага ўліковага запісу спонсарства тарыфнага плана Bitwarden Families завяршыцца ў канцы плацежнага перыяду. У вас не будзе магчымасці скарыстацца новай спонсарскай прапановай, пакуль не завяршыцца тэрмін бягучай прапановы. Вы ўпэўнены, што хочаце працягнуць?" + "message": "Пасля выдалення гэтага ўліковага запісу, спонсарства тарыфнага плана Bitwarden Families завяршыцца ў канцы плацежнага перыяду. У вас не будзе магчымасці скарыстацца новай спонсарскай прапановай, пакуль не завяршыцца тэрмін бягучай прапановы. Вы ўпэўнены, што хочаце працягнуць?" }, "removeSponsorshipSuccess": { - "message": "Спансаванне выдалена" + "message": "Спансіраванне выдалена" }, "ssoKeyConnectorError": { "message": "Памылка Key Connector: пераканайцеся, што Key Connector даступны і карэктна працуе." @@ -4942,7 +4942,7 @@ "message": "Key Connector адключаны" }, "keyConnectorWarning": { - "message": "Як толькі ўдзельнікі пачнуць выкарыстоўваць Key Connector, ваша арганізацыя не зможа вярнуцца да расшыфроўкі з асноўным паролем. Працягвайце толькі калі вы гатовы разгарнуць сервер ключоў і кіраваць ім." + "message": "Пасля таго, як удзельнікі пачнуць выкарыстоўваць Key Connector, ваша арганізацыя не зможа вярнуцца да расшыфроўкі з асноўным паролем. Працягвайце толькі калі вы гатовы разгарнуць сервер ключоў і кіраваць ім." }, "migratedKeyConnector": { "message": "Выканана міграцыя на Key Connector" @@ -4978,7 +4978,7 @@ "message": "БЯСПЛАТНА дзякуючы спонсарству" }, "viewBillingSyncToken": { - "message": "Паглядзець плацежны токен сінхранізацыі" + "message": "Прагледзець плацежны токен сінхранізацыі" }, "generateBillingSyncToken": { "message": "Генерыраваць плацежны токен сінхранізацыі" @@ -4990,7 +4990,7 @@ "message": "Ваш токен плацежнай сінхранізацыі можа атрымаць доступ і адрэдагаваць налады падпіскі гэтай арганізацыі." }, "manageBillingSync": { - "message": "Кіраваць плацежнай сінхранізацыяй" + "message": "Кіраванне плацежнай сінхранізацыяй" }, "setUpBillingSync": { "message": "Наладзіць плацежную сінхранізацыю" @@ -5011,7 +5011,7 @@ "message": "Уласнае размяшчэнне" }, "selfHostingEnterpriseOrganizationSectionCopy": { - "message": "Для наладжвання арганізацыі на вашым уласным серверы, вам неабходна будзе загрузіць файл ліцэнзіі. Для падтрымкі тарыфных планаў Bitwarden Families і пашыраных магчымасцяў выстаўлення рахункаў для вашай арганізацыі, вам неабходна наладзіць плацежную сінхранізацыю." + "message": "Для наладжвання арганізацыі на вашым уласным серверы, вам неабходна будзе загрузіць файл з ліцэнзіяй. Для падтрымкі тарыфных планаў Bitwarden Families і пашыраных магчымасцяў выстаўлення рахункаў для вашай арганізацыі, вам неабходна наладзіць плацежную сінхранізацыю." }, "billingSyncApiKeyRotated": { "message": "Токен зменены." @@ -5068,7 +5068,7 @@ } }, "required": { - "message": "абавязкова" + "message": "патрабуецца" }, "idpSingleSignOnServiceUrlRequired": { "message": "Патрабуецца, калі ідэнтыфікатар аб'екта не з'яўляецца URL-адрасам." @@ -5077,19 +5077,19 @@ "message": "Дадатковыя дапасаванні" }, "openIdAuthorityRequired": { - "message": "Абавязкова, калі ўстанова не дзейнічае." + "message": "Патрабуецца, калі ўстанова не дзейнічае." }, "separateMultipleWithComma": { - "message": "Некалькі значэнняў, якія раздзелыны коскай." + "message": "Некалькі значэнняў, якія раздзелены коскай." }, "sessionTimeout": { - "message": "Час чакання вашай сесіі завяршыўся. Калі ласка, увайдзіце паўторна." + "message": "Час чакання вашага сеанса завяршыўся. Калі ласка, увайдзіце паўторна." }, "exportingPersonalVaultTitle": { "message": "Экспартаванне асабістага сховішча" }, "exportingOrganizationVaultTitle": { - "message": "Экспартавання сховішча арганізацыі" + "message": "Экспартаванне сховішча арганізацыі" }, "exportingPersonalVaultDescription": { "message": "Будуць экспартаваны толькі асабістыя элементы сховішча, якія звязаны з $EMAIL$. Элементы сховішча арганізацыі не будуць уключаны.", @@ -5101,7 +5101,7 @@ } }, "exportingOrganizationVaultDescription": { - "message": "Будуць экспартаваны толькі запісы сховішча арганізацыі, які звязаны з $ORGANIZATION$. Элементы асабістага сховішча і элементы з іншых арганізацый не будуць уключаны.", + "message": "Будуць экспартаваны толькі запісы сховішча арганізацыі, якія звязаны з $ORGANIZATION$. Элементы асабістага сховішча і элементы з іншых арганізацый не будуць уключаны.", "placeholders": { "organization": { "content": "$1", @@ -5166,7 +5166,7 @@ "message": "Адрас для ўсёй пошты дамена" }, "catchallEmailDesc": { - "message": "Выкарыстоўвайце сваю сканфігураваную скрыню для ўсё пошты дамена." + "message": "Выкарыстоўвайце сваю сканфігураваную скрыню для ўсёй пошты дамена." }, "random": { "message": "Выпадкова", @@ -5182,7 +5182,7 @@ "message": "Невядомы элемент. Магчыма, што вам неабходна запытаць дазвол на доступ да гэтага элемента." }, "cannotSponsorSelf": { - "message": "Вы не можаце актываваць актыўны ўліковы запіс. Увядзіце іншы адрас электроннай пошты." + "message": "Вы не можаце актываваць бягучы ўліковы запіс. Увядзіце іншы адрас электроннай пошты." }, "revokeWhenExpired": { "message": "Міне $DATE$", @@ -5216,7 +5216,7 @@ "Description": "Used as a prefix to indicate the last time a sync occured. Example \"Last sync 1968-11-16 00:00:00\"" }, "sponsorshipsSynced": { - "message": "Уласнае размяшчэнне спансавання сінхранізавана." + "message": "Уласнае размяшчэнне спансіравання сінхранізавана." }, "billingManagedByProvider": { "message": "Кіруецца $PROVIDER$", diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index 419ace835ea..65134d43fc5 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -2257,7 +2257,7 @@ "message": "Годишно" }, "annual": { - "message": "Annual" + "message": "Годишно" }, "basePrice": { "message": "Базова цена" diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index a8324937f85..54b778b228b 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -918,7 +918,7 @@ "message": "Set a password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "fileTypeHeading": { - "message": "File Type" + "message": "Τύπος αρχείου" }, "accountBackup": { "message": "Account Backup" @@ -1721,7 +1721,7 @@ "message": "Billing Plan" }, "paymentType": { - "message": "Payment Type" + "message": "Τύπος πληρωμής" }, "accountCredit": { "message": "Λογαριασμός Πίστωσης", @@ -4424,10 +4424,10 @@ "message": "Αφαίρεση Χρηστών" }, "revokeUsers": { - "message": "Revoke Users" + "message": "Ανάκληση χρηστών" }, "restoreUsers": { - "message": "Restore Users" + "message": "Επαναφορά χρηστών" }, "error": { "message": "Σφάλμα" @@ -5116,13 +5116,13 @@ "message": "Master Password" }, "security": { - "message": "Security" + "message": "Ασφάλεια" }, "keys": { - "message": "Keys" + "message": "Κλειδιά" }, "billingHistory": { - "message": "Billing History" + "message": "Ιστορικό χρέωσης" }, "backToReports": { "message": "Επιστροφή στις Αναφορές" @@ -5135,7 +5135,7 @@ "description": "This is used by screen readers to indicate the organization that is currently being shown to the user." }, "accountSettings": { - "message": "Account Settings" + "message": "Ρυθμίσεις λογαριασμού" }, "generator": { "message": "Γεννήτρια" @@ -5303,7 +5303,7 @@ "message": "Rotate Key" }, "scimApiKey": { - "message": "SCIM API Key", + "message": "Κλειδί API SCIM", "description": "the text, 'SCIM' and 'API', are acronymns and should not be translated." }, "copyScimUrl": { @@ -5347,7 +5347,7 @@ } }, "turnOn": { - "message": "Turn on" + "message": "Ενεργοποίηση" }, "on": { "message": "On" diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index b064002d08e..61086889919 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -2248,7 +2248,7 @@ } }, "trialConfirmationEmail": { - "message": "Olemme lähettäneet vahvistusviestin tiimisi laskutussähköpostiosoitteeseen " + "message": "Olemme lähettäneet sähköpostivahvistuksen tiimisi laskutusoitteeseen " }, "monthly": { "message": "Kuukausittainen" @@ -2326,10 +2326,10 @@ "message": "Haluatko varmasti poistaa käyttäjän?" }, "removeOrgUserConfirmation": { - "message": "Kun jäsen poistetaan, hänellä ei ole enää pääsyä organisaation tietoihin, eikä poistoa ole mahdollista perua. Jos haluat lisätä jäsenen takaisin organisaatioon, hänet on kutsuttava ja perehdytettävä uudelleen." + "message": "Poisteltulla jäsenellä ei ole enää organisaation tietojen käyttöoikeutta, eikä poiston peruminen ole mahdollista. Jos jäsen halutaan palauttaa organisaatioon, hänet on kutsuttava ja määritettävä uudelleen." }, "revokeUserConfirmation": { - "message": "Kun jäsenen käyttöoikeus perutaan, hänellä ei ole enää pääsyä organisaation tietoihin. Voit palauttaa käyttöoikeuden nopeasti Perutut-välilehdeltä." + "message": "Perutulla jäsenellä ei ole enää organisaation tietojen käyttöoikeutta. Käyttöoikeus voidaan palauttaa nopeasti Perutut-välilehdeltä." }, "removeUserConfirmationKeyConnector": { "message": "Varoitus! Tämä käyttäjä tarvitsee salauksensa hallintaan Key Connectoria. Käyttäjän poistaminen organisaatiostasi poistaa heidän tilinsä käytöstä pysvästi. Toimenpide on pysyvä, eikä sen peruminen ole mahdollista. Haluatko jatkaa?" @@ -2395,10 +2395,10 @@ "message": "Hae" }, "invited": { - "message": "Kutsuttu" + "message": "Kutsutut" }, "accepted": { - "message": "Hyväksytty" + "message": "Hyväksytyt" }, "confirmed": { "message": "Vahvistettu" @@ -2686,7 +2686,7 @@ } }, "revokedUserId": { - "message": "Jäsenen $ID$ organisaation käyttöoikeus peruttiin.", + "message": "Käyttäjän $ID$ organisaation käyttöoikeus on peruttu.", "placeholders": { "id": { "content": "$1", @@ -2704,7 +2704,7 @@ } }, "revokeUserId": { - "message": "Peru jäsenen $ID$ käyttöoikeus", + "message": "Peru käyttäjän $ID$ käyttöoikeus", "placeholders": { "id": { "content": "$1", @@ -4023,7 +4023,7 @@ "message": "Käytäntöä ei pakoteta käyttöön niille organisaation käyttäjille, joilla on organisaation käytäntöjen hallintaoikeudet." }, "disableHideEmail": { - "message": "Näytä jäsenen sähköpostiosoite aina vastaanottajien kanssa, kun luodaan tai muokataan Sendiä.", + "message": "Näytä jäsenen sähköpostiosoite aina vastaanottajien ohessa, kun Send luodaan tai sitä muokataan.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendOptionsPolicyInEffect": { @@ -4370,13 +4370,13 @@ "message": "Toiminto ei koske valittuja käyttäjiä." }, "removeUsersWarning": { - "message": "Haluatko varmasti poistaa seuraavat käyttäjät? Toiminto saattaa kestää muutamia sekunteja, eikä sen keskeytys tai peruminen ole mahdollista." + "message": "Haluatko varmasti poistaa seuraavat käyttäjät? Käsittely voi kestää muutamia sekunteja, eikä sen keskeytys tai peruminen ole mahdollista." }, "removeOrgUsersConfirmation": { - "message": "Kun jäseniä poistetaan, heillä ei ole enää pääsyä organisaation tietoihin, eikä poistoa ole mahdollista perua. Jos haluat lisätä jäsenen takaisin organisaatioon, hänet on kutsuttava ja perehdytettävä uudelleen. Käsittely voi kestää muutamia sekunteja, eikä sitä ole mahdollista keskeyttää tai perua." + "message": "Poisteltuilla jäsenillä ei ole enää organisaation tietojen käyttöoikeutta, eikä poiston peruminen ole mahdollista. Jos jäsniä halutaan palauttaa organisaatioon, heidät on kutsuttava ja määritettävä uudelleen. Käsittely voi kestää muutamia sekunteja, eikä sen keskeytys tai peruminen ole mahdollista." }, "revokeUsersWarning": { - "message": "Kun jäsenien käyttöoikeudet perutaan, heillä ei ole enää pääsyä organisaation tietoihin. Voit palauttaa käyttöoikeudet nopeasti Perutut-välilehdeltä. Käsittely voi kestää muutamia sekunteja, eikä sitä ole mahdollista keskeyttää tai perua." + "message": "Perutuilla jäsenillä ei ole enää organisaation tietojen käyttöoikeutta. Käyttöoikeudet voidaan palauttaa nopeasti Perutut-välilehdeltä. Käsittely voi kestää muutamia sekunteja, eikä sen keskeytys tai peruminen ole mahdollista." }, "theme": { "message": "Teema" diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index f1d428388a6..7409f55b9c0 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -679,7 +679,7 @@ "message": "Nederīga galvenā parole" }, "invalidFilePassword": { - "message": "Invalid file password, please use the password you entered when you created the export file." + "message": "Nederīga datnes parole, lūgums izmantot to paroli, kas tika ievadīta izdošanas datnes izveidošanas brīdī." }, "lockNow": { "message": "Aizslēgt" @@ -894,46 +894,46 @@ "message": "Datnes veids" }, "fileEncryptedExportWarningDesc": { - "message": "This file export will be password protected and require the file password to decrypt." + "message": "Šī datņu izdošana būs aizsargāta ar paroli, un būs nepieciešama datnes parole, lai to atšifrētu." }, "exportPasswordDescription": { - "message": "This password will be used to export and import this file" + "message": "Šī parole tiks izmantota, lai izdotu un ievietotu šo datni" }, "confirmMasterPassword": { - "message": "Confirm Master Password" + "message": "Apstiprināt galveno paroli" }, "confirmFormat": { - "message": "Confirm Format" + "message": "Apstiprināt veidolu" }, "filePassword": { - "message": "File Password" + "message": "Datnes parole" }, "confirmFilePassword": { - "message": "Confirm File Password" + "message": "Apstiprināt datnes paroli" }, "accountBackupOptionDescription": { - "message": "Use your account encryption key to encrypt the export and restrict import to only the current Bitwarden account." + "message": "Jāizmanto konta šifrēšanas atslēga, lai šifrētu izdošanu un ierobežotu ievietošanu tikai pašreizējā Bitwarden kontā." }, "passwordProtectedOptionDescription": { - "message": "Set a password to encrypt the export and import it to any Bitwarden account using the password for decryption." + "message": "Uzstādīt paroli, lai šifrētu izdošanu un tad to ievietotu jebkurā Bitwarden kontā, izmantojot atšifrēšanas paroli." }, "fileTypeHeading": { - "message": "File Type" + "message": "Datnes veids" }, "accountBackup": { - "message": "Account Backup" + "message": "Konta rezerves kopija" }, "passwordProtected": { - "message": "Password Protected" + "message": "Aizsargāts ar paroli" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm File Password“ do not match." + "message": "\"Datnes parole\" un \"Apstiprināt datnes paroli\" vērtības nesakrīt." }, "confirmVaultImport": { - "message": "Confirm Vault Import" + "message": "Apstiprināt glabātavas satura ievietošanu" }, "confirmVaultImportDesc": { - "message": "This file is password-protected. Please enter the file password to import data." + "message": "Šī datne ir aizsargāta ar paroli. Lūgums ievadīt datnes paroli, lai ievietotu datus." }, "exportSuccess": { "message": "Glabātavas saturs ir izgūts." diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 464af5736fa..d866fc6512b 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -912,10 +912,10 @@ "message": "Підтвердьте пароль файлу" }, "accountBackupOptionDescription": { - "message": "Завжди використовує шифрування облікового запису Bitwarden, а не головний пароль для захисту експорту. Цей експорт можна імпортувати лише в поточний обліковий запис. Використовуйте його для створення резервної копії, яку не можна використовувати в іншому місці." + "message": "Використовуйте ключ шифрування облікового запису, щоб зашифрувати експортований файл і дозволити його імпорт лише до поточного облікового запису Bitwarden." }, "passwordProtectedOptionDescription": { - "message": "Створити користувацький пароль для захисту експорту. Використовуйте це для створення експорту, який може бути використаний в інших облікових записах." + "message": "Встановіть пароль, щоб зашифрувати експортований файл та дозволити імпорт до будь-якого облікового запису Bitwarden за допомогою цього пароля." }, "fileTypeHeading": { "message": "Тип файлу" From 174603b484981a6aa237785d08a4cbf2a4941cf2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:06:02 +0200 Subject: [PATCH 05/23] Autosync the updated translations (#3582) Co-authored-by: github-actions <> --- apps/desktop/src/locales/az/messages.json | 2 +- apps/desktop/src/locales/bg/messages.json | 2 +- apps/desktop/src/locales/da/messages.json | 2 +- apps/desktop/src/locales/el/messages.json | 10 +++++----- apps/desktop/src/locales/eu/messages.json | 2 +- apps/desktop/src/locales/pl/messages.json | 6 +++--- apps/desktop/src/locales/ro/messages.json | 2 +- apps/desktop/src/locales/sr/messages.json | 2 +- apps/desktop/src/locales/tr/messages.json | 2 +- apps/desktop/src/locales/uk/messages.json | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index 164c41c3634..acc41c106ce 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Anbar" } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 60512db2efa..03c198fc422 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -1995,6 +1995,6 @@ "message": "Мир" }, "vault": { - "message": "Vault" + "message": "Трезор" } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 03f30ee3a58..fd755046059 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Boks" } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 592e6a4febd..d5703316ff3 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -533,7 +533,7 @@ "message": "Μη έγκυρη διεύθυνση e-mail." }, "masterPasswordRequired": { - "message": "Master password is required." + "message": "Απαιτείται κύριος κωδικός πρόσβασης." }, "confirmMasterPasswordRequired": { "message": "Master password retype is required." @@ -898,7 +898,7 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Show website icons" + "message": "Εμφάνιση εικονιδίων ιστοτόπων" }, "faviconDesc": { "message": "Show a recognizable image next to each login." @@ -1394,7 +1394,7 @@ "message": "Κλείδωμα με τον κύριο κωδικό πρόσβασης κατά την επανεκκίνηση" }, "deleteAccount": { - "message": "Delete account" + "message": "Διαγραφή λογαριασμού" }, "deleteAccountDesc": { "message": "Proceed below to delete your account and all vault data." @@ -1403,7 +1403,7 @@ "message": "Deleting your account is permanent. It cannot be undone." }, "accountDeleted": { - "message": "Account deleted" + "message": "Ο λογαριασμός διαγράφηκε" }, "accountDeletedDesc": { "message": "Your account has been closed and all associated data has been deleted." @@ -1980,7 +1980,7 @@ "message": "Kλειδί API" }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Απαιτείται συνδρομή Premium" }, "organizationIsDisabled": { "message": "Organization is disabled." diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index f4fcbe5cb54..c5b3b8d4f06 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Kutxa gotorra" } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index 2c982c95f3c..d6d32e15570 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -872,16 +872,16 @@ "message": "4 godziny" }, "onIdle": { - "message": "Po bezczynności przez 5 minut" + "message": "Podczas bezczynności systemu" }, "onSleep": { - "message": "Po uśpieniu komputera" + "message": "Podczas uśpienia systemu" }, "onLocked": { "message": "Po zablokowaniu komputera" }, "onRestart": { - "message": "Po uruchomieniu ponownym" + "message": "Po restarcie aplikacji" }, "never": { "message": "Nigdy" diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index dd8ddc9430c..2b772cd4d7c 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Seif" } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index a41737282d7..f4ca5f10610 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Сеф" } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 9bd360c0af4..5195e788105 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -1989,7 +1989,7 @@ "message": "Devre dışı kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." }, "neverLockWarning": { - "message": "Are you sure you want to use the \"Never\" option? Setting your lock options to \"Never\" stores your vault's encryption key on your device. If you use this option you should ensure that you keep your device properly protected." + "message": "\"Asla\" seçeneğini kullanmak istediğinizden emin misiniz? Kilit seçeneklerinizi \"Asla\" olarak ayarlamak, kasanızın şifreleme anahtarını cihazınızda saklar. Bu seçeneği kullanırsanız, cihazınızı uygun şekilde koruduğunuzdan emin olmalısınız." }, "cardBrandMir": { "message": "Mir" diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 09bff4f23b8..6eb04464e93 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Сховище" } } From 0be448b4da2baf64b9a38abe9fcbfcbc24f0dd45 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Sep 2022 19:07:01 +0200 Subject: [PATCH 06/23] Autosync the updated translations (#3581) Co-authored-by: github-actions <> --- apps/browser/src/_locales/ar/messages.json | 104 +++++++++--------- apps/browser/src/_locales/el/messages.json | 2 +- apps/browser/src/_locales/pt_BR/messages.json | 20 ++-- apps/browser/src/_locales/sr/messages.json | 8 +- apps/browser/src/_locales/tr/messages.json | 2 +- apps/browser/store/locales/fi/copy.resx | 2 +- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index bbcdf2e2b26..34abd9594f5 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -157,7 +157,7 @@ "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." }, "twoStepLogin": { - "message": "Two-step login" + "message": "تسجيل الدخول بخطوتين" }, "logOut": { "message": "تسجيل الخروج" @@ -258,7 +258,7 @@ "description": "Make the first letter of a work uppercase." }, "includeNumber": { - "message": "Include Number" + "message": "تضمين الرقم" }, "minNumbers": { "message": "الحد الأدنى من الأرقام" @@ -315,7 +315,7 @@ "message": "عرض العنصر" }, "launch": { - "message": "Launch" + "message": "بدء" }, "website": { "message": "الموقع الإلكتروني" @@ -327,7 +327,7 @@ "message": "إدارة" }, "other": { - "message": "Other" + "message": "الأخرى" }, "rateExtension": { "message": "قيِّم هذه الإضافة" @@ -412,7 +412,7 @@ "message": "مطلقاً" }, "security": { - "message": "Security" + "message": "الأمان" }, "errorOccurred": { "message": "لقد حدث خطأ ما" @@ -488,10 +488,10 @@ "message": "تغيير كلمة المرور الرئيسية" }, "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" }, "twoStepLoginConfirmation": { - "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" + "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, "editedFolder": { "message": "Edited folder" @@ -540,22 +540,22 @@ "message": "Edited item" }, "deleteItemConfirmation": { - "message": "Do you really want to send to the trash?" + "message": "هل تريد حقاً أن ترسل إلى سلة المهملات؟" }, "deletedItem": { "message": "تم إرسال العنصر إلى سلة المهملات" }, "overwritePassword": { - "message": "Overwrite Password" + "message": "الكتابة فوق كلمة المرور" }, "overwritePasswordConfirmation": { - "message": "Are you sure you want to overwrite the current password?" + "message": "هل أنت متأكد من أنك تريد الكتابة فوق كلمة المرور الموجودة؟" }, "overwriteUsername": { - "message": "Overwrite Username" + "message": "الكتابة فوق اسم المستخدم" }, "overwriteUsernameConfirmation": { - "message": "Are you sure you want to overwrite the current username?" + "message": "هل أنت متأكد من أنك تريد الكتابة فوق اسم المستخدم الموجود؟" }, "searchFolder": { "message": "إبحث في المجلّد" @@ -589,15 +589,15 @@ "message": "List identity items on the Tab page for easy auto-fill." }, "clearClipboard": { - "message": "Clear clipboard", + "message": "مسح الحافظة", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "clearClipboardDesc": { - "message": "Automatically clear copied values from your clipboard.", + "message": "مسح القيم المنسوخة تلقائيًا من حافظتك.", "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "notificationAddDesc": { - "message": "Should Bitwarden remember this password for you?" + "message": "هل يجب على Bitwarden تذكر كلمة المرور هذه لك؟" }, "notificationAddSave": { "message": "حفظ" @@ -683,10 +683,10 @@ "message": "Move to Organization" }, "share": { - "message": "Share" + "message": "مشاركة" }, "movedItemToOrg": { - "message": "$ITEMNAME$ moved to $ORGNAME$", + "message": "$ITEMNAME$ انتقل إلى $ORGNAME$", "placeholders": { "itemname": { "content": "$1", @@ -699,16 +699,16 @@ } }, "moveToOrgDesc": { - "message": "Choose an organization that you wish to move this item to. Moving to an organization transfers ownership of the item to that organization. You will no longer be the direct owner of this item once it has been moved." + "message": "اختر المؤسسة التي ترغب في نقل هذا العنصر إليها. يؤدي النقل إلى مؤسسة إلى نقل ملكية العنصر إلى تلك المؤسسة. لن تكون المالك المباشر لهذا العنصر بعد نقله." }, "learnMore": { - "message": "Learn more" + "message": "معرفة المزيد" }, "authenticatorKeyTotp": { - "message": "Authenticator Key (TOTP)" + "message": "مفتاح المصادقة (TOTP)" }, "verificationCodeTotp": { - "message": "Verification Code (TOTP)" + "message": "رمز التحقق (TOTP)" }, "copyVerificationCode": { "message": "Copy Verification Code" @@ -1124,16 +1124,16 @@ "message": "Dr" }, "firstName": { - "message": "First Name" + "message": "الاسم الأول" }, "middleName": { - "message": "Middle Name" + "message": "الاسم الأوسط" }, "lastName": { - "message": "Last Name" + "message": "الاسم الأخير" }, "fullName": { - "message": "Full Name" + "message": "الاسم الكامل" }, "identityName": { "message": "Identity Name" @@ -1289,7 +1289,7 @@ "description": "The URI of one of the current open tabs in the browser." }, "organization": { - "message": "Organization", + "message": "المؤسسة", "description": "An entity of multiple related people (ex. a team or business organization)." }, "types": { @@ -1672,17 +1672,17 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "expirationDate": { - "message": "Expiration Date" + "message": "تاريخ انتهاء الصلاحية" }, "expirationDateDesc": { "message": "If set, access to this Send will expire on the specified date and time.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "oneDay": { - "message": "1 day" + "message": "يوم واحد" }, "days": { - "message": "$DAYS$ days", + "message": "$DAYS$ أيام", "placeholders": { "days": { "content": "$1", @@ -1731,7 +1731,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "newPassword": { - "message": "New Password" + "message": "كلمة المرور الجديدة" }, "sendDisabled": { "message": "Send Disabled", @@ -1766,7 +1766,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read '**To use a calendar style date picker ** click here to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage2": { - "message": "click here", + "message": "انقر هنا", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To use a calendar style date picker **click here** to pop out your window.'" }, "sendFirefoxCustomDatePopoutMessage3": { @@ -1798,37 +1798,37 @@ "message": "Master password re-prompt" }, "passwordConfirmation": { - "message": "Master password confirmation" + "message": "تأكيد كلمة المرور الرئيسية" }, "passwordConfirmationDesc": { - "message": "This action is protected. To continue, please re-enter your master password to verify your identity." + "message": "هذا الإجراء محمي. للاستمرار، يرجى إعادة إدخال كلمة المرور الرئيسية للتحقق من هويتك." }, "emailVerificationRequired": { - "message": "Email Verification Required" + "message": "تأكيد البريد الإلكتروني مطلوب" }, "emailVerificationRequiredDesc": { - "message": "You must verify your email to use this feature. You can verify your email in the web vault." + "message": "يجب عليك تأكيد بريدك الإلكتروني لاستخدام هذه الميزة. يمكنك تأكيد بريدك الإلكتروني في خزنة الويب." }, "updatedMasterPassword": { "message": "Updated Master Password" }, "updateMasterPassword": { - "message": "Update Master Password" + "message": "تحديث كلمة المرور الرئيسية" }, "updateMasterPasswordWarning": { - "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "تم تغيير كلمة المرور الرئيسية الخاصة بك مؤخرًا من قبل مسؤول في مؤسستك. من أجل الوصول إلى الخزنة، يجب عليك تحديثها الآن. سيتم تسجيل خروجك من الجلسة الحالية، مما يتطلب منك تسجيل الدخول مرة أخرى. قد تظل الجلسات النشطة على أجهزة أخرى نشطة لمدة تصل إلى ساعة واحدة." }, "resetPasswordPolicyAutoEnroll": { - "message": "Automatic Enrollment" + "message": "التسجيل التلقائي" }, "resetPasswordAutoEnrollInviteWarning": { - "message": "This organization has an enterprise policy that will automatically enroll you in password reset. Enrollment will allow organization administrators to change your master password." + "message": "هذه المؤسسة لديها سياسة تقوم تلقائياً بتسجيلك في إعادة تعيين كلمة المرور. هذا التسجيل سيسمح لمسؤولي المؤسسة بتغيير كلمة المرور الرئيسية الخاصة بك." }, "selectFolder": { "message": "Select folder..." }, "ssoCompleteRegistration": { - "message": "In order to complete logging in with SSO, please set a master password to access and protect your vault." + "message": "من أجل إكمال تسجيل الدخول باستخدام SSO، يرجى تعيين كلمة المرور الرئيسية للوصول لخزنتك وحمايتها." }, "hours": { "message": "ساعات" @@ -1837,7 +1837,7 @@ "message": "دقائق" }, "vaultTimeoutPolicyInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed Vault Timeout is $HOURS$ hour(s) and $MINUTES$ minute(s)", + "message": "سياسات مؤسستك تؤثر على مهلة الخزنة الخاص بك. الحد الأقصى المسموح به لمهلة الخزنة هو $HOURS$ ساعة/ساعات و $MINUTES$ دقيقة/دقائق", "placeholders": { "hours": { "content": "$1", @@ -1850,19 +1850,19 @@ } }, "vaultTimeoutTooLarge": { - "message": "Your vault timeout exceeds the restrictions set by your organization." + "message": "مهلة خزنتك تتجاوز القيود التي تضعها مؤسستك." }, "vaultExportDisabled": { - "message": "Vault Export Disabled" + "message": "تصدير الخزنة مُعطّل" }, "personalVaultExportPolicyInEffect": { - "message": "One or more organization policies prevents you from exporting your personal vault." + "message": "واحدة أو أكثر من سياسات المؤسسة تمنعك من تصدير خزانتك الشخصية." }, "copyCustomFieldNameInvalidElement": { - "message": "Unable to identify a valid form element. Try inspecting the HTML instead." + "message": "غير قادر على التعرف على نموذج صالح. حاول فحص HTML بدلا من ذلك." }, "copyCustomFieldNameNotUnique": { - "message": "No unique identifier found." + "message": "لم يتم العثور على معرف فريد." }, "convertOrganizationEncryptionDesc": { "message": "$ORGANIZATION$ is using SSO with a self-hosted key server. A master password is no longer required to log in for members of this organization.", @@ -1923,10 +1923,10 @@ "description": "Username generator option that appends a random sub-address to the username. For example: address+subaddress@email.com" }, "plusAddressedEmailDesc": { - "message": "Use your email provider's sub-addressing capabilities." + "message": "استخدم قدرات العنوان الفرعي لمزود البريد الإلكتروني الخاص بك." }, "catchallEmail": { - "message": "Catch-all Email" + "message": "تجميع كل البريد الإلكتروني" }, "catchallEmailDesc": { "message": "Use your domain's configured catch-all inbox." @@ -1950,10 +1950,10 @@ "message": "الخدمة" }, "forwardedEmail": { - "message": "Forwarded Email Alias" + "message": "إعادة توجيه الاسم المستعار للبريد الإلكتروني" }, "forwardedEmailDesc": { - "message": "Generate an email alias with an external forwarding service." + "message": "إنشاء بريد إلكتروني مستعار مع خدمة إعادة توجيه خارجية." }, "hostname": { "message": "اسم المضيف", @@ -1993,7 +1993,7 @@ "message": "تم تعديل الإعدادات" }, "environmentEditedClick": { - "message": "Click here" + "message": "انقر هنا" }, "environmentEditedReset": { "message": "لإعادة تعيين الإعدادات المُعدة مسبقاً" @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "آخر ظهور في $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 378c6c28167..6674489c638 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1999,7 +1999,7 @@ "message": "επαναφορά στις προ-ρυθμισμένες ρυθμίσεις" }, "serverVersion": { - "message": "Server Version" + "message": "Έκδοση διακομιστή" }, "selfHosted": { "message": "Self-Hosted" diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a81faf65245..4de3a20245b 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1969,19 +1969,19 @@ "message": "Erro de Key Connector: certifique-se de que a Key Connector está disponível e funcionando corretamente." }, "premiumSubcriptionRequired": { - "message": "Premium subscription required" + "message": "Assinatura Premium necessária" }, "organizationIsDisabled": { - "message": "Organization is disabled." + "message": "Organização está desabilitada." }, "disabledOrganizationFilterError": { - "message": "Items in disabled Organizations cannot be accessed. Contact your Organization owner for assistance." + "message": "Itens em Organizações Desativadas não podem ser acessados. Entre em contato com o proprietário da sua Organização para obter assistência." }, "cardBrandMir": { "message": "Mir" }, "loggingInTo": { - "message": "Logging in to $DOMAIN$", + "message": "Fazendo login em $DOMAIN$", "placeholders": { "domain": { "content": "$1", @@ -1990,22 +1990,22 @@ } }, "settingsEdited": { - "message": "Settings have been edited" + "message": "As configurações foram editadas" }, "environmentEditedClick": { - "message": "Click here" + "message": "Clique aqui" }, "environmentEditedReset": { - "message": "to reset to pre-configured settings" + "message": "para redefinir para as configurações pré-configuradas" }, "serverVersion": { - "message": "Server Version" + "message": "Versão do servidor" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Auto-hospedado" }, "thirdParty": { - "message": "Third-Party" + "message": "Terceiros" }, "thirdPartyServerMessage": { "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index b2b6ca8c7aa..ab2033e54f0 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1999,13 +1999,13 @@ "message": "за рисетовање на подразумевана подешавања" }, "serverVersion": { - "message": "Server Version" + "message": "Верзија сервера" }, "selfHosted": { - "message": "Self-Hosted" + "message": "Личан хостинг" }, "thirdParty": { - "message": "Third-Party" + "message": "Трећа страна" }, "thirdPartyServerMessage": { "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "последње виђено у $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index be7bbefb8dd..38d6a28475a 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2008,7 +2008,7 @@ "message": "Üçüncü Taraf" }, "thirdPartyServerMessage": { - "message": "Connected to third-party server implementation, $SERVERNAME$. Please verify bugs using the official server, or report them to the third-party server.", + "message": "Üçüncü taraf sunucu uygulamasına bağlandı, $SERVERNAME$. Lütfen resmi sunucuyu kullanarak hataları doğrulayın veya üçüncü taraf sunucuya bildirin.", "placeholders": { "servername": { "content": "$1", diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 5a85be3b48a..7511ed5915e 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -134,7 +134,7 @@ Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. -Yrityksille Bitwarden tarjoaa Teams- ja Enterprise-tilaukset, jotka mahdollistavat turvallisen salasanojen jaon kollegoiden kesken. +Yrityksille Bitwarden tarjoaa Teams- ja Enterprise-tilaukset, jotka mahdollistavat turvallisen salasanojen jakamisen kollegoiden kesken. Miksi Bitwarden?: From 30f38dc916348336d21c7a10d1b766881cf0fcf7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Sep 2022 22:25:36 -0400 Subject: [PATCH 07/23] Bumped web version to 2022.9.2 (#3586) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- apps/web/package.json | 2 +- package-lock.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 4f6a2a0e802..cc146e56123 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2022.9.1", + "version": "2022.9.2", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/package-lock.json b/package-lock.json index eb2ea47d6b3..a0f5a037f80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -233,7 +233,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2022.9.1" + "version": "2022.9.2" }, "libs/angular": { "name": "@bitwarden/angular", From df9e6e21c9312494921c4d652110512603a0a43c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 22 Sep 2022 07:51:14 -0500 Subject: [PATCH 08/23] Ps 1291/apply to from json pattern to state (#3425) * Clean up dangling behaviorSubject * Handle null in utils * fix null check * Await promises, even in async functions * Add to/fromJSON methods to State and Accounts This is needed since all storage in manifest v3 is key-value-pair-based and session storage of most data is actually serialized into an encrypted string. * Simplify AccountKeys json parsing * Fix account key (de)serialization * Remove unused DecodedToken state * Correct filename typo * Simplify keys `toJSON` tests * Explain AccountKeys `toJSON` return type * Remove unnecessary `any`s * Remove unique ArrayBuffer serialization * Initialize items in MemoryStorageService * Revert "Fix account key (de)serialization" This reverts commit b1dffb5c2cb7c02feec079704af52f7d9b07359b, which was breaking serializations * Move fromJSON to owning object * Add DeepJsonify type * Use Records for storage * Add new Account Settings to serialized data * Fix failing serialization tests * Extract complex type conversion to helper methods * Remove unnecessary decorator * Return null from json deserializers * Remove unnecessary decorators * Remove obsolete test * Use type-fest `Jsonify` formatting rules for external library * Update jsonify comment Co-authored-by: @eliykat * Remove erroneous comment * Fix unintended deep-jsonify changes * Fix prettierignore * Fix formatting of deep-jsonify.ts Co-authored-by: Thomas Rittson Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> --- .prettierignore | 3 + .../session-sync.decorator.spec.ts | 1 + .../session-syncer.spec.ts | 5 +- .../session-sync-observable/session-syncer.ts | 4 +- .../sync-item-metadata.ts | 18 +-- .../synced-item-metadata.spec.ts | 56 +++---- .../services/abstractions/state.service.ts | 4 +- .../localBackedSessionStorage.service.spec.ts | 7 + .../localBackedSessionStorage.service.ts | 26 +++- .../src/services/state.service.spec.ts | 10 +- apps/browser/src/services/state.service.ts | 6 +- libs/common/spec/misc/utils.spec.ts | 6 + .../spec/services/state.service.spec.ts | 82 ---------- .../services/stateMigration.service.spec.ts | 4 +- libs/common/src/abstractions/state.service.ts | 2 - .../src/abstractions/storage.service.ts | 8 +- libs/common/src/misc/utils.ts | 3 + .../models/data/server-config.data.spec.ts | 55 +++++++ .../src/models/data/server-config.data.ts | 35 +++-- .../src/models/domain/account-keys.spec.ts | 62 ++++++++ .../src/models/domain/account-profile.spec.ts | 9 ++ .../models/domain/account-settings.spec.ts | 24 +++ .../src/models/domain/account-tokens.spec.ts | 9 ++ libs/common/src/models/domain/account.spec.ts | 23 +++ libs/common/src/models/domain/account.ts | 141 ++++++++++++++++- .../src/models/domain/encryption-pair.spec.ts | 34 +++++ .../src/models/domain/environmentUrls.ts | 6 + libs/common/src/models/domain/state.spec.ts | 28 ++++ libs/common/src/models/domain/state.ts | 28 ++++ .../src/models/domain/storageOptions.ts | 4 + libs/common/src/services/cipher.service.ts | 2 +- libs/common/src/services/encrypt.service.ts | 2 +- .../src/services/memoryStorage.service.ts | 10 +- libs/common/src/services/state.service.ts | 143 +++++------------- .../src/services/stateMigration.service.ts | 12 +- libs/common/src/services/token.service.ts | 5 - libs/common/src/types/deep-jsonify.ts | 44 ++++++ 37 files changed, 635 insertions(+), 286 deletions(-) delete mode 100644 libs/common/spec/services/state.service.spec.ts create mode 100644 libs/common/src/models/data/server-config.data.spec.ts create mode 100644 libs/common/src/models/domain/account-keys.spec.ts create mode 100644 libs/common/src/models/domain/account-profile.spec.ts create mode 100644 libs/common/src/models/domain/account-settings.spec.ts create mode 100644 libs/common/src/models/domain/account-tokens.spec.ts create mode 100644 libs/common/src/models/domain/account.spec.ts create mode 100644 libs/common/src/models/domain/encryption-pair.spec.ts create mode 100644 libs/common/src/models/domain/state.spec.ts create mode 100644 libs/common/src/types/deep-jsonify.ts diff --git a/.prettierignore b/.prettierignore index 8790a1e73ff..b1c9359fa1e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,3 +27,6 @@ libs/.github # Github Workflows .github/workflows + +# Forked library files +libs/common/src/types/deep-jsonify.ts diff --git a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts index c82f2700213..b177d118f82 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-sync.decorator.spec.ts @@ -19,6 +19,7 @@ describe("sessionSync decorator", () => { ctor: ctor, initializer: initializer, }), + testClass.testProperty.complete(), ]); }); }); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts index f2df60ad7a4..5286cece1bb 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.spec.ts @@ -5,6 +5,7 @@ import { BrowserApi } from "../../browser/browserApi"; import { StateService } from "../../services/abstractions/state.service"; import { SessionSyncer } from "./session-syncer"; +import { SyncedItemMetadata } from "./sync-item-metadata"; describe("session syncer", () => { const propertyKey = "behaviorSubject"; @@ -140,12 +141,14 @@ describe("session syncer", () => { }); it("should update from message on emit from another instance", async () => { + const builder = jest.fn(); + jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); stateService.getFromSessionMemory.mockResolvedValue("test"); await sut.updateFromMessage({ command: `${sessionKey}_update`, id: "different_id" }); expect(stateService.getFromSessionMemory).toHaveBeenCalledTimes(1); - expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey); + expect(stateService.getFromSessionMemory).toHaveBeenCalledWith(sessionKey, builder); expect(nextSpy).toHaveBeenCalledTimes(1); expect(nextSpy).toHaveBeenCalledWith("test"); diff --git a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts index 0c97b983f75..c757a44c7f2 100644 --- a/apps/browser/src/decorators/session-sync-observable/session-syncer.ts +++ b/apps/browser/src/decorators/session-sync-observable/session-syncer.ts @@ -66,8 +66,8 @@ export class SessionSyncer { if (message.command != this.updateMessageCommand || message.id === this.id) { return; } - const keyValuePair = await this.stateService.getFromSessionMemory(this.metaData.sessionKey); - const value = SyncedItemMetadata.buildFromKeyValuePair(keyValuePair, this.metaData); + const builder = SyncedItemMetadata.builder(this.metaData); + const value = await this.stateService.getFromSessionMemory(this.metaData.sessionKey, builder); this.ignoreNextUpdate = true; this.behaviorSubject.next(value); } diff --git a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts index e225db61967..2b3f4715d46 100644 --- a/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts +++ b/apps/browser/src/decorators/session-sync-observable/sync-item-metadata.ts @@ -5,19 +5,15 @@ export class SyncedItemMetadata { initializer?: (keyValuePair: any) => any; initializeAsArray?: boolean; - static buildFromKeyValuePair(keyValuePair: any, metadata: SyncedItemMetadata): any { - const builder = SyncedItemMetadata.getBuilder(metadata); - + static builder(metadata: SyncedItemMetadata): (o: any) => any { + const itemBuilder = + metadata.initializer != null + ? metadata.initializer + : (o: any) => Object.assign(new metadata.ctor(), o); if (metadata.initializeAsArray) { - return keyValuePair.map((o: any) => builder(o)); + return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); } else { - return builder(keyValuePair); + return (keyValuePair: any) => itemBuilder(keyValuePair); } } - - private static getBuilder(metadata: SyncedItemMetadata): (o: any) => any { - return metadata.initializer != null - ? metadata.initializer - : (o: any) => Object.assign(new metadata.ctor(), o); - } } diff --git a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts index da65be04903..5cd869a5b67 100644 --- a/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ b/apps/browser/src/decorators/session-sync-observable/synced-item-metadata.spec.ts @@ -1,59 +1,39 @@ import { SyncedItemMetadata } from "./sync-item-metadata"; -describe("build from key value pair", () => { +describe("builder", () => { const propertyKey = "propertyKey"; const key = "key"; const initializer = (s: any) => "used initializer"; class TestClass {} const ctor = TestClass; - it("should call initializer if provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: "key", - initializer: initializer, - } - ); - - expect(actual).toEqual("used initializer"); + it("should use initializer if provided", () => { + const metadata = { propertyKey, sessionKey: key, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); - it("should call ctor if provided", () => { - const expected = { provided: "value" }; - const actual = SyncedItemMetadata.buildFromKeyValuePair(expected, { - propertyKey, - sessionKey: key, - ctor: ctor, - }); - - expect(actual).toBeInstanceOf(ctor); - expect(actual).toEqual(expect.objectContaining(expected)); + it("should use ctor if initializer is not provided", () => { + const metadata = { propertyKey, sessionKey: key, ctor }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBeInstanceOf(TestClass); }); - it("should prefer using initializer if both are provided", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair( - {}, - { - propertyKey, - sessionKey: key, - initializer: initializer, - ctor: ctor, - } - ); - - expect(actual).toEqual("used initializer"); + it("should prefer initializer over ctor", () => { + const metadata = { propertyKey, sessionKey: key, ctor, initializer }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder({})).toBe("used initializer"); }); it("should honor initialize as array", () => { - const actual = SyncedItemMetadata.buildFromKeyValuePair([1, 2], { + const metadata = { propertyKey, sessionKey: key, initializer: initializer, initializeAsArray: true, - }); - - expect(actual).toEqual(["used initializer", "used initializer"]); + }; + const builder = SyncedItemMetadata.builder(metadata); + expect(builder([{}])).toBeInstanceOf(Array); + expect(builder([{}])[0]).toBe("used initializer"); }); }); diff --git a/apps/browser/src/services/abstractions/state.service.ts b/apps/browser/src/services/abstractions/state.service.ts index ca2a5ced7fa..2f0112ff64f 100644 --- a/apps/browser/src/services/abstractions/state.service.ts +++ b/apps/browser/src/services/abstractions/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/abstractions/state.service"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -7,7 +9,7 @@ import { BrowserGroupingsComponentState } from "src/models/browserGroupingsCompo import { BrowserSendComponentState } from "src/models/browserSendComponentState"; export abstract class StateService extends BaseStateServiceAbstraction { - abstract getFromSessionMemory(key: string): Promise; + abstract getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise; abstract setInSessionMemory(key: string, value: any): Promise; getBrowserGroupingComponentState: ( options?: StorageOptions diff --git a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts index de88b6d8b2f..f7101ddae2e 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.spec.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.spec.ts @@ -96,6 +96,13 @@ describe("Browser Session Storage Service", () => { expect(cache.has("test")).toBe(true); expect(cache.get("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); + }); }); }); }); diff --git a/apps/browser/src/services/localBackedSessionStorage.service.ts b/apps/browser/src/services/localBackedSessionStorage.service.ts index cdc7d4cf154..dea2e75a5ed 100644 --- a/apps/browser/src/services/localBackedSessionStorage.service.ts +++ b/apps/browser/src/services/localBackedSessionStorage.service.ts @@ -1,6 +1,12 @@ +import { Jsonify } from "type-fest"; + import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; -import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; +import { + AbstractCachedStorageService, + MemoryStorageServiceInterface, +} from "@bitwarden/common/abstractions/storage.service"; import { EncString } from "@bitwarden/common/models/domain/encString"; +import { MemoryStorageOptions } from "@bitwarden/common/models/domain/storageOptions"; import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; import { devFlag } from "../decorators/dev-flag.decorator"; @@ -15,7 +21,10 @@ const keys = { sessionKey: "session", }; -export class LocalBackedSessionStorageService extends AbstractCachedStorageService { +export class LocalBackedSessionStorageService + extends AbstractCachedStorageService + implements MemoryStorageServiceInterface +{ private cache = new Map(); private localStorage = new BrowserLocalStorageService(); private sessionStorage = new BrowserMemoryStorageService(); @@ -27,21 +36,26 @@ export class LocalBackedSessionStorageService extends AbstractCachedStorageServi super(); } - async get(key: string): Promise { + async get(key: string, options?: MemoryStorageOptions): Promise { if (this.cache.has(key)) { return this.cache.get(key) as T; } - return await this.getBypassCache(key); + return await this.getBypassCache(key, options); } - async getBypassCache(key: string): Promise { + async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { const session = await this.getLocalSession(await this.getSessionEncKey()); if (session == null || !Object.keys(session).includes(key)) { return null; } - this.cache.set(key, session[key]); + let value = session[key]; + if (options?.deserializer != null) { + value = options.deserializer(value as Jsonify); + } + + this.cache.set(key, value); return this.cache.get(key) as T; } diff --git a/apps/browser/src/services/state.service.spec.ts b/apps/browser/src/services/state.service.spec.ts index 60813c22931..f3b6c74a5e3 100644 --- a/apps/browser/src/services/state.service.spec.ts +++ b/apps/browser/src/services/state.service.spec.ts @@ -1,8 +1,8 @@ -import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { - AbstractCachedStorageService, + MemoryStorageServiceInterface, AbstractStorageService, } from "@bitwarden/common/abstractions/storage.service"; import { SendType } from "@bitwarden/common/enums/sendType"; @@ -49,7 +49,7 @@ describe("Browser State Service", () => { }); describe("direct memory storage access", () => { - let memoryStorageService: AbstractCachedStorageService; + let memoryStorageService: LocalBackedSessionStorageService; beforeEach(() => { // We need `AbstractCachedStorageService` in the prototype chain to correctly test cache bypass. @@ -79,12 +79,12 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: SubstituteOf; + let memoryStorageService: SubstituteOf; beforeEach(() => { memoryStorageService = Substitute.for(); const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); - memoryStorageService.get("state").mimicks(stateGetter); + memoryStorageService.get("state", Arg.any()).mimicks(stateGetter); sut = new StateService( diskStorageService, diff --git a/apps/browser/src/services/state.service.ts b/apps/browser/src/services/state.service.ts index 6685f495e06..78bc721031a 100644 --- a/apps/browser/src/services/state.service.ts +++ b/apps/browser/src/services/state.service.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { AbstractCachedStorageService } from "@bitwarden/common/abstractions/storage.service"; import { GlobalState } from "@bitwarden/common/models/domain/globalState"; import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; @@ -17,9 +19,9 @@ export class StateService extends BaseStateService implements StateServiceAbstraction { - async getFromSessionMemory(key: string): Promise { + async getFromSessionMemory(key: string, deserializer?: (obj: Jsonify) => T): Promise { return this.memoryStorageService instanceof AbstractCachedStorageService - ? await this.memoryStorageService.getBypassCache(key) + ? await this.memoryStorageService.getBypassCache(key, { deserializer: deserializer }) : await this.memoryStorageService.get(key); } diff --git a/libs/common/spec/misc/utils.spec.ts b/libs/common/spec/misc/utils.spec.ts index d4dbac958a8..3978f9cfed8 100644 --- a/libs/common/spec/misc/utils.spec.ts +++ b/libs/common/spec/misc/utils.spec.ts @@ -70,4 +70,10 @@ describe("Utils Service", () => { expect(Utils.newGuid()).toMatch(validGuid); }); }); + + describe("fromByteStringToArray", () => { + it("should handle null", () => { + expect(Utils.fromByteStringToArray(null)).toEqual(null); + }); + }); }); diff --git a/libs/common/spec/services/state.service.spec.ts b/libs/common/spec/services/state.service.spec.ts deleted file mode 100644 index a6f76ab4fc0..00000000000 --- a/libs/common/spec/services/state.service.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; - -import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; -import { StateFactory } from "@bitwarden/common/factories/stateFactory"; -import { Account } from "@bitwarden/common/models/domain/account"; -import { GlobalState } from "@bitwarden/common/models/domain/globalState"; -import { State } from "@bitwarden/common/models/domain/state"; -import { StorageOptions } from "@bitwarden/common/models/domain/storageOptions"; -import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; -import { StateService } from "@bitwarden/common/services/state.service"; -import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; - -describe("Browser State Service backed by chrome.storage api", () => { - let secureStorageService: SubstituteOf; - let diskStorageService: SubstituteOf; - let memoryStorageService: SubstituteOf; - let logService: SubstituteOf; - let stateMigrationService: SubstituteOf; - let stateFactory: SubstituteOf>; - let useAccountCache: boolean; - - let state: State; - const userId = "userId"; - - let sut: StateService; - - beforeEach(() => { - secureStorageService = Substitute.for(); - diskStorageService = Substitute.for(); - memoryStorageService = Substitute.for(); - logService = Substitute.for(); - stateMigrationService = Substitute.for(); - stateFactory = Substitute.for(); - useAccountCache = true; - - state = new State(new GlobalState()); - const stateGetter = (key: string) => Promise.resolve(JSON.parse(JSON.stringify(state))); - memoryStorageService.get("state").mimicks(stateGetter); - memoryStorageService - .save("state", Arg.any(), Arg.any()) - .mimicks((key: string, obj: any, options: StorageOptions) => { - return new Promise(() => { - state = obj; - }); - }); - - sut = new StateService( - diskStorageService, - secureStorageService, - memoryStorageService, - logService, - stateMigrationService, - stateFactory, - useAccountCache - ); - }); - - describe("account state getters", () => { - beforeEach(() => { - state.accounts[userId] = createAccount(userId); - state.activeUserId = userId; - }); - - describe("getCryptoMasterKey", () => { - it("should return the stored SymmetricCryptoKey", async () => { - const key = new SymmetricCryptoKey(new Uint8Array(32).buffer); - state.accounts[userId].keys.cryptoMasterKey = key; - - const actual = await sut.getCryptoMasterKey(); - expect(actual).toBeInstanceOf(SymmetricCryptoKey); - expect(actual).toMatchObject(key); - }); - }); - }); - - function createAccount(userId: string): Account { - return new Account({ - profile: { userId: userId }, - }); - } -}); diff --git a/libs/common/spec/services/stateMigration.service.spec.ts b/libs/common/spec/services/stateMigration.service.spec.ts index e306e64e29b..b0188abacce 100644 --- a/libs/common/spec/services/stateMigration.service.spec.ts +++ b/libs/common/spec/services/stateMigration.service.spec.ts @@ -116,8 +116,8 @@ describe("State Migration Service", () => { key: "orgThreeEncKey", }, }, - }, - }, + } as any, + } as any, }); const migratedAccount = await (stateMigrationService as any).migrateAccountFrom4To5( diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index 238dfe199dd..94e24da419a 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -78,8 +78,6 @@ export abstract class StateService { getCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; hasCryptoMasterKeyBiometric: (options?: StorageOptions) => Promise; setCryptoMasterKeyBiometric: (value: string, options?: StorageOptions) => Promise; - getDecodedToken: (options?: StorageOptions) => Promise; - setDecodedToken: (value: any, options?: StorageOptions) => Promise; getDecryptedCiphers: (options?: StorageOptions) => Promise; setDecryptedCiphers: (value: CipherView[], options?: StorageOptions) => Promise; getDecryptedCollections: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/abstractions/storage.service.ts b/libs/common/src/abstractions/storage.service.ts index 5cff9fdac60..31506f4302b 100644 --- a/libs/common/src/abstractions/storage.service.ts +++ b/libs/common/src/abstractions/storage.service.ts @@ -1,4 +1,4 @@ -import { StorageOptions } from "../models/domain/storageOptions"; +import { MemoryStorageOptions, StorageOptions } from "../models/domain/storageOptions"; export abstract class AbstractStorageService { abstract get(key: string, options?: StorageOptions): Promise; @@ -8,5 +8,9 @@ export abstract class AbstractStorageService { } export abstract class AbstractCachedStorageService extends AbstractStorageService { - abstract getBypassCache(key: string, options?: StorageOptions): Promise; + abstract getBypassCache(key: string, options?: MemoryStorageOptions): Promise; +} + +export interface MemoryStorageServiceInterface { + get(key: string, options?: MemoryStorageOptions): Promise; } diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index f66124bc47e..454def36551 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -99,6 +99,9 @@ export class Utils { } static fromByteStringToArray(str: string): Uint8Array { + if (str == null) { + return null; + } const arr = new Uint8Array(str.length); for (let i = 0; i < str.length; i++) { arr[i] = str.charCodeAt(i); diff --git a/libs/common/src/models/data/server-config.data.spec.ts b/libs/common/src/models/data/server-config.data.spec.ts new file mode 100644 index 00000000000..1c4e890ab80 --- /dev/null +++ b/libs/common/src/models/data/server-config.data.spec.ts @@ -0,0 +1,55 @@ +import { + EnvironmentServerConfigData, + ServerConfigData, + ThirdPartyServerConfigData, +} from "./server-config.data"; + +describe("ServerConfigData", () => { + describe("fromJSON", () => { + it("should create a ServerConfigData from a JSON object", () => { + const serverConfigData = ServerConfigData.fromJSON({ + version: "1.0.0", + gitHash: "1234567890", + server: { + name: "test", + url: "https://test.com", + }, + environment: { + vault: "https://vault.com", + api: "https://api.com", + identity: "https://identity.com", + notifications: "https://notifications.com", + sso: "https://sso.com", + }, + utcDate: "2020-01-01T00:00:00.000Z", + }); + + expect(serverConfigData.version).toEqual("1.0.0"); + expect(serverConfigData.gitHash).toEqual("1234567890"); + expect(serverConfigData.server.name).toEqual("test"); + expect(serverConfigData.server.url).toEqual("https://test.com"); + expect(serverConfigData.environment.vault).toEqual("https://vault.com"); + expect(serverConfigData.environment.api).toEqual("https://api.com"); + expect(serverConfigData.environment.identity).toEqual("https://identity.com"); + expect(serverConfigData.environment.notifications).toEqual("https://notifications.com"); + expect(serverConfigData.environment.sso).toEqual("https://sso.com"); + expect(serverConfigData.utcDate).toEqual("2020-01-01T00:00:00.000Z"); + }); + + it("should be an instance of ServerConfigData", () => { + const serverConfigData = ServerConfigData.fromJSON({} as any); + + expect(serverConfigData).toBeInstanceOf(ServerConfigData); + }); + + it("should deserialize sub objects", () => { + const serverConfigData = ServerConfigData.fromJSON({ + server: {}, + environment: {}, + } as any); + + expect(serverConfigData.server).toBeInstanceOf(ThirdPartyServerConfigData); + expect(serverConfigData.environment).toBeInstanceOf(EnvironmentServerConfigData); + }); + }); +}); diff --git a/libs/common/src/models/data/server-config.data.ts b/libs/common/src/models/data/server-config.data.ts index 62744ecb621..30043a0b028 100644 --- a/libs/common/src/models/data/server-config.data.ts +++ b/libs/common/src/models/data/server-config.data.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { ServerConfigResponse, ThirdPartyServerConfigResponse, @@ -11,27 +13,38 @@ export class ServerConfigData { environment?: EnvironmentServerConfigData; utcDate: string; - constructor(serverConfigReponse: ServerConfigResponse) { - this.version = serverConfigReponse?.version; - this.gitHash = serverConfigReponse?.gitHash; - this.server = serverConfigReponse?.server - ? new ThirdPartyServerConfigData(serverConfigReponse.server) + constructor(serverConfigResponse: Partial) { + this.version = serverConfigResponse?.version; + this.gitHash = serverConfigResponse?.gitHash; + this.server = serverConfigResponse?.server + ? new ThirdPartyServerConfigData(serverConfigResponse.server) : null; this.utcDate = new Date().toISOString(); - this.environment = serverConfigReponse?.environment - ? new EnvironmentServerConfigData(serverConfigReponse.environment) + this.environment = serverConfigResponse?.environment + ? new EnvironmentServerConfigData(serverConfigResponse.environment) : null; } + + static fromJSON(obj: Jsonify): ServerConfigData { + return Object.assign(new ServerConfigData({}), obj, { + server: obj?.server ? ThirdPartyServerConfigData.fromJSON(obj.server) : null, + environment: obj?.environment ? EnvironmentServerConfigData.fromJSON(obj.environment) : null, + }); + } } export class ThirdPartyServerConfigData { name: string; url: string; - constructor(response: ThirdPartyServerConfigResponse) { + constructor(response: Partial) { this.name = response.name; this.url = response.url; } + + static fromJSON(obj: Jsonify): ThirdPartyServerConfigData { + return Object.assign(new ThirdPartyServerConfigData({}), obj); + } } export class EnvironmentServerConfigData { @@ -41,11 +54,15 @@ export class EnvironmentServerConfigData { notifications: string; sso: string; - constructor(response: EnvironmentServerConfigResponse) { + constructor(response: Partial) { this.vault = response.vault; this.api = response.api; this.identity = response.identity; this.notifications = response.notifications; this.sso = response.sso; } + + static fromJSON(obj: Jsonify): EnvironmentServerConfigData { + return Object.assign(new EnvironmentServerConfigData({}), obj); + } } diff --git a/libs/common/src/models/domain/account-keys.spec.ts b/libs/common/src/models/domain/account-keys.spec.ts new file mode 100644 index 00000000000..bf8348e15a2 --- /dev/null +++ b/libs/common/src/models/domain/account-keys.spec.ts @@ -0,0 +1,62 @@ +import { Utils } from "@bitwarden/common/misc/utils"; + +import { makeStaticByteArray } from "../../../spec/utils"; + +import { AccountKeys, EncryptionPair } from "./account"; +import { SymmetricCryptoKey } from "./symmetricCryptoKey"; + +describe("AccountKeys", () => { + describe("toJSON", () => { + it("should serialize itself", () => { + const keys = new AccountKeys(); + const buffer = makeStaticByteArray(64).buffer; + keys.publicKey = buffer; + + const bufferSpy = jest.spyOn(Utils, "fromBufferToByteString"); + keys.toJSON(); + expect(bufferSpy).toHaveBeenCalledWith(buffer); + }); + + it("should serialize public key as a string", () => { + const keys = new AccountKeys(); + keys.publicKey = Utils.fromByteStringToArray("hello").buffer; + const json = JSON.stringify(keys); + expect(json).toContain('"publicKey":"hello"'); + }); + }); + + describe("fromJSON", () => { + it("should deserialize public key to a buffer", () => { + const keys = AccountKeys.fromJSON({ + publicKey: "hello", + }); + expect(keys.publicKey).toEqual(Utils.fromByteStringToArray("hello").buffer); + }); + + it("should deserialize cryptoMasterKey", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({} as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize organizationKeys", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({ organizationKeys: [{ orgId: "keyJSON" }] } as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize providerKeys", () => { + const spy = jest.spyOn(SymmetricCryptoKey, "fromJSON"); + AccountKeys.fromJSON({ providerKeys: [{ providerId: "keyJSON" }] } as any); + expect(spy).toHaveBeenCalled(); + }); + + it("should deserialize privateKey", () => { + const spy = jest.spyOn(EncryptionPair, "fromJSON"); + AccountKeys.fromJSON({ + privateKey: { encrypted: "encrypted", decrypted: "decrypted" }, + } as any); + expect(spy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-profile.spec.ts b/libs/common/src/models/domain/account-profile.spec.ts new file mode 100644 index 00000000000..7c6deda34eb --- /dev/null +++ b/libs/common/src/models/domain/account-profile.spec.ts @@ -0,0 +1,9 @@ +import { AccountProfile } from "./account"; + +describe("AccountProfile", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountProfile.fromJSON({})).toBeInstanceOf(AccountProfile); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-settings.spec.ts b/libs/common/src/models/domain/account-settings.spec.ts new file mode 100644 index 00000000000..544d591b323 --- /dev/null +++ b/libs/common/src/models/domain/account-settings.spec.ts @@ -0,0 +1,24 @@ +import { AccountSettings, EncryptionPair } from "./account"; +import { EncString } from "./encString"; + +describe("AccountSettings", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings); + }); + + it("should deserialize pinProtected", () => { + const accountSettings = new AccountSettings(); + accountSettings.pinProtected = EncryptionPair.fromJSON({ + encrypted: "encrypted", + decrypted: "3.data", + }); + const jsonObj = JSON.parse(JSON.stringify(accountSettings)); + const actual = AccountSettings.fromJSON(jsonObj); + + expect(actual.pinProtected).toBeInstanceOf(EncryptionPair); + expect(actual.pinProtected.encrypted).toEqual("encrypted"); + expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data"); + }); + }); +}); diff --git a/libs/common/src/models/domain/account-tokens.spec.ts b/libs/common/src/models/domain/account-tokens.spec.ts new file mode 100644 index 00000000000..733b3908e9a --- /dev/null +++ b/libs/common/src/models/domain/account-tokens.spec.ts @@ -0,0 +1,9 @@ +import { AccountTokens } from "./account"; + +describe("AccountTokens", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); + }); + }); +}); diff --git a/libs/common/src/models/domain/account.spec.ts b/libs/common/src/models/domain/account.spec.ts new file mode 100644 index 00000000000..0c76c16cc2d --- /dev/null +++ b/libs/common/src/models/domain/account.spec.ts @@ -0,0 +1,23 @@ +import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; + +describe("Account", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(Account.fromJSON({})).toBeInstanceOf(Account); + }); + + it("should call all the sub-fromJSONs", () => { + const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); + const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); + const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); + const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); + + Account.fromJSON({}); + + expect(keysSpy).toHaveBeenCalled(); + expect(profileSpy).toHaveBeenCalled(); + expect(settingsSpy).toHaveBeenCalled(); + expect(tokensSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/account.ts b/libs/common/src/models/domain/account.ts index b7bbcb431c9..5822af35c03 100644 --- a/libs/common/src/models/domain/account.ts +++ b/libs/common/src/models/domain/account.ts @@ -1,3 +1,8 @@ +import { Except, Jsonify } from "type-fest"; + +import { Utils } from "@bitwarden/common/misc/utils"; +import { DeepJsonify } from "@bitwarden/common/types/deep-jsonify"; + import { AuthenticationStatus } from "../../enums/authenticationStatus"; import { KdfType } from "../../enums/kdfType"; import { UriMatchType } from "../../enums/uriMatchType"; @@ -24,7 +29,39 @@ import { SymmetricCryptoKey } from "./symmetricCryptoKey"; export class EncryptionPair { encrypted?: TEncrypted; decrypted?: TDecrypted; - decryptedSerialized?: string; + + toJSON() { + return { + encrypted: this.encrypted, + decrypted: + this.decrypted instanceof ArrayBuffer + ? Utils.fromBufferToByteString(this.decrypted) + : this.decrypted, + }; + } + + static fromJSON( + obj: Jsonify, Jsonify>>, + decryptedFromJson?: (decObj: Jsonify | string) => TDecrypted, + encryptedFromJson?: (encObj: Jsonify) => TEncrypted + ) { + if (obj == null) { + return null; + } + + const pair = new EncryptionPair(); + if (obj?.encrypted != null) { + pair.encrypted = encryptedFromJson + ? encryptedFromJson(obj.encrypted) + : (obj.encrypted as TEncrypted); + } + if (obj?.decrypted != null) { + pair.decrypted = decryptedFromJson + ? decryptedFromJson(obj.decrypted) + : (obj.decrypted as TDecrypted); + } + return pair; + } } export class DataEncryptionPair { @@ -73,19 +110,66 @@ export class AccountKeys { >(); organizationKeys?: EncryptionPair< { [orgId: string]: EncryptedOrganizationKeyData }, - Map + Record > = new EncryptionPair< { [orgId: string]: EncryptedOrganizationKeyData }, - Map + Record >(); - providerKeys?: EncryptionPair> = new EncryptionPair< + providerKeys?: EncryptionPair> = new EncryptionPair< any, - Map + Record >(); privateKey?: EncryptionPair = new EncryptionPair(); publicKey?: ArrayBuffer; - publicKeySerialized?: string; apiKeyClientSecret?: string; + + toJSON() { + return Object.assign(this as Except, { + publicKey: Utils.fromBufferToByteString(this.publicKey), + }); + } + + static fromJSON(obj: DeepJsonify): AccountKeys { + if (obj == null) { + return null; + } + + return Object.assign( + new AccountKeys(), + { cryptoMasterKey: SymmetricCryptoKey.fromJSON(obj?.cryptoMasterKey) }, + { + cryptoSymmetricKey: EncryptionPair.fromJSON( + obj?.cryptoSymmetricKey, + SymmetricCryptoKey.fromJSON + ), + }, + { organizationKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.organizationKeys) }, + { providerKeys: AccountKeys.initRecordEncryptionPairsFromJSON(obj?.providerKeys) }, + { + privateKey: EncryptionPair.fromJSON( + obj?.privateKey, + (decObj: string) => Utils.fromByteStringToArray(decObj).buffer + ), + }, + { + publicKey: Utils.fromByteStringToArray(obj?.publicKey)?.buffer, + } + ); + } + + static initRecordEncryptionPairsFromJSON(obj: any) { + return EncryptionPair.fromJSON(obj, (decObj: any) => { + if (obj == null) { + return null; + } + + const record: Record = {}; + for (const id in decObj) { + record[id] = SymmetricCryptoKey.fromJSON(decObj[id]); + } + return record; + }); + } } export class AccountProfile { @@ -106,6 +190,14 @@ export class AccountProfile { keyHash?: string; kdfIterations?: number; kdfType?: KdfType; + + static fromJSON(obj: Jsonify): AccountProfile { + if (obj == null) { + return null; + } + + return Object.assign(new AccountProfile(), obj); + } } export class AccountSettings { @@ -142,6 +234,21 @@ export class AccountSettings { vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; serverConfig?: ServerConfigData; + + static fromJSON(obj: Jsonify): AccountSettings { + if (obj == null) { + return null; + } + + return Object.assign(new AccountSettings(), obj, { + environmentUrls: EnvironmentUrls.fromJSON(obj?.environmentUrls), + pinProtected: EncryptionPair.fromJSON( + obj?.pinProtected, + EncString.fromJSON + ), + serverConfig: ServerConfigData.fromJSON(obj?.serverConfig), + }); + } } export type AccountSettingsSettings = { @@ -150,9 +257,16 @@ export type AccountSettingsSettings = { export class AccountTokens { accessToken?: string; - decodedToken?: any; refreshToken?: string; securityStamp?: string; + + static fromJSON(obj: Jsonify): AccountTokens { + if (obj == null) { + return null; + } + + return Object.assign(new AccountTokens(), obj); + } } export class Account { @@ -186,4 +300,17 @@ export class Account { }, }); } + + static fromJSON(json: Jsonify): Account { + if (json == null) { + return null; + } + + return Object.assign(new Account({}), json, { + keys: AccountKeys.fromJSON(json?.keys), + profile: AccountProfile.fromJSON(json?.profile), + settings: AccountSettings.fromJSON(json?.settings), + tokens: AccountTokens.fromJSON(json?.tokens), + }); + } } diff --git a/libs/common/src/models/domain/encryption-pair.spec.ts b/libs/common/src/models/domain/encryption-pair.spec.ts new file mode 100644 index 00000000000..55fad76db03 --- /dev/null +++ b/libs/common/src/models/domain/encryption-pair.spec.ts @@ -0,0 +1,34 @@ +import { Utils } from "@bitwarden/common/misc/utils"; + +import { EncryptionPair } from "./account"; + +describe("EncryptionPair", () => { + describe("toJSON", () => { + it("should populate decryptedSerialized for buffer arrays", () => { + const pair = new EncryptionPair(); + pair.decrypted = Utils.fromByteStringToArray("hello").buffer; + const json = pair.toJSON(); + expect(json.decrypted).toEqual("hello"); + }); + + it("should serialize encrypted and decrypted", () => { + const pair = new EncryptionPair(); + pair.encrypted = "hello"; + pair.decrypted = "world"; + const json = pair.toJSON(); + expect(json.encrypted).toEqual("hello"); + expect(json.decrypted).toEqual("world"); + }); + }); + + describe("fromJSON", () => { + it("should deserialize encrypted and decrypted", () => { + const pair = EncryptionPair.fromJSON({ + encrypted: "hello", + decrypted: "world", + }); + expect(pair.encrypted).toEqual("hello"); + expect(pair.decrypted).toEqual("world"); + }); + }); +}); diff --git a/libs/common/src/models/domain/environmentUrls.ts b/libs/common/src/models/domain/environmentUrls.ts index d4fd173c83a..c78768bdc29 100644 --- a/libs/common/src/models/domain/environmentUrls.ts +++ b/libs/common/src/models/domain/environmentUrls.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + export class EnvironmentUrls { base: string = null; api: string = null; @@ -7,4 +9,8 @@ export class EnvironmentUrls { events: string = null; webVault: string = null; keyConnector: string = null; + + static fromJSON(obj: Jsonify): EnvironmentUrls { + return Object.assign(new EnvironmentUrls(), obj); + } } diff --git a/libs/common/src/models/domain/state.spec.ts b/libs/common/src/models/domain/state.spec.ts new file mode 100644 index 00000000000..64e71d7cb2c --- /dev/null +++ b/libs/common/src/models/domain/state.spec.ts @@ -0,0 +1,28 @@ +import { Account } from "./account"; +import { State } from "./state"; + +describe("state", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(State.fromJSON({})).toBeInstanceOf(State); + }); + + it("should always assign an object to accounts", () => { + const state = State.fromJSON({}); + expect(state.accounts).not.toBeNull(); + expect(state.accounts).toEqual({}); + }); + + it("should build an account map", () => { + const accountsSpy = jest.spyOn(Account, "fromJSON"); + const state = State.fromJSON({ + accounts: { + userId: {}, + }, + }); + + expect(state.accounts["userId"]).toBeInstanceOf(Account); + expect(accountsSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/libs/common/src/models/domain/state.ts b/libs/common/src/models/domain/state.ts index f5a2c046b54..5450325d25c 100644 --- a/libs/common/src/models/domain/state.ts +++ b/libs/common/src/models/domain/state.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { Account } from "./account"; import { GlobalState } from "./globalState"; @@ -14,4 +16,30 @@ export class State< constructor(globals: TGlobalState) { this.globals = globals; } + + // TODO, make Jsonify work. It currently doesn't because Globals doesn't implement Jsonify. + static fromJSON( + obj: any + ): State { + if (obj == null) { + return null; + } + + return Object.assign(new State(null), obj, { + accounts: State.buildAccountMapFromJSON(obj?.accounts), + }); + } + + private static buildAccountMapFromJSON( + jsonAccounts: Jsonify<{ [userId: string]: Jsonify }> + ) { + if (!jsonAccounts) { + return {}; + } + const accounts: { [userId: string]: Account } = {}; + for (const userId in jsonAccounts) { + accounts[userId] = Account.fromJSON(jsonAccounts[userId]); + } + return accounts; + } } diff --git a/libs/common/src/models/domain/storageOptions.ts b/libs/common/src/models/domain/storageOptions.ts index 2db7e0ccf58..7e21b194332 100644 --- a/libs/common/src/models/domain/storageOptions.ts +++ b/libs/common/src/models/domain/storageOptions.ts @@ -1,3 +1,5 @@ +import { Jsonify } from "type-fest"; + import { HtmlStorageLocation } from "../../enums/htmlStorageLocation"; import { StorageLocation } from "../../enums/storageLocation"; @@ -8,3 +10,5 @@ export type StorageOptions = { htmlStorageLocation?: HtmlStorageLocation; keySuffix?: string; }; + +export type MemoryStorageOptions = StorageOptions & { deserializer?: (obj: Jsonify) => T }; diff --git a/libs/common/src/services/cipher.service.ts b/libs/common/src/services/cipher.service.ts index 40440ea8bf7..62d849d4176 100644 --- a/libs/common/src/services/cipher.service.ts +++ b/libs/common/src/services/cipher.service.ts @@ -393,7 +393,7 @@ export class CipherService implements CipherServiceAbstraction { : firstValueFrom(this.settingsService.settings$).then( (settings: AccountSettingsSettings) => { let matches: any[] = []; - settings.equivalentDomains.forEach((eqDomain: any) => { + settings.equivalentDomains?.forEach((eqDomain: any) => { if (eqDomain.length && eqDomain.indexOf(domain) >= 0) { matches = matches.concat(eqDomain); } diff --git a/libs/common/src/services/encrypt.service.ts b/libs/common/src/services/encrypt.service.ts index 0b3f9731ca7..fef33a3f9c3 100644 --- a/libs/common/src/services/encrypt.service.ts +++ b/libs/common/src/services/encrypt.service.ts @@ -98,7 +98,7 @@ export class EncryptService implements AbstractEncryptService { } } - return this.cryptoFunctionService.aesDecryptFast(fastParams); + return await this.cryptoFunctionService.aesDecryptFast(fastParams); } async decryptToBytes(encThing: IEncrypted, key: SymmetricCryptoKey): Promise { diff --git a/libs/common/src/services/memoryStorage.service.ts b/libs/common/src/services/memoryStorage.service.ts index d1616c6029f..1634b67b051 100644 --- a/libs/common/src/services/memoryStorage.service.ts +++ b/libs/common/src/services/memoryStorage.service.ts @@ -1,6 +1,12 @@ -import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; +import { + AbstractStorageService, + MemoryStorageServiceInterface, +} from "@bitwarden/common/abstractions/storage.service"; -export class MemoryStorageService implements AbstractStorageService { +export class MemoryStorageService + extends AbstractStorageService + implements MemoryStorageServiceInterface +{ private store = new Map(); get(key: string): Promise { diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index d5aabb3490a..7b6e59eb2cd 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -3,14 +3,16 @@ import { BehaviorSubject, concatMap } from "rxjs"; import { LogService } from "../abstractions/log.service"; import { StateService as StateServiceAbstraction } from "../abstractions/state.service"; import { StateMigrationService } from "../abstractions/stateMigration.service"; -import { AbstractStorageService } from "../abstractions/storage.service"; +import { + MemoryStorageServiceInterface, + AbstractStorageService, +} from "../abstractions/storage.service"; import { HtmlStorageLocation } from "../enums/htmlStorageLocation"; import { KdfType } from "../enums/kdfType"; import { StorageLocation } from "../enums/storageLocation"; import { ThemeType } from "../enums/themeType"; import { UriMatchType } from "../enums/uriMatchType"; import { StateFactory } from "../factories/stateFactory"; -import { Utils } from "../misc/utils"; import { CipherData } from "../models/data/cipherData"; import { CollectionData } from "../models/data/collectionData"; import { EncryptedOrganizationKeyData } from "../models/data/encryptedOrganizationKeyData"; @@ -76,7 +78,7 @@ export class StateService< constructor( protected storageService: AbstractStorageService, protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractStorageService, + protected memoryStorageService: AbstractStorageService & MemoryStorageServiceInterface, protected logService: LogService, protected stateMigrationService: StateMigrationService, protected stateFactory: StateFactory, @@ -150,6 +152,9 @@ export class StateService< return; } await this.updateState(async (state) => { + if (state.accounts == null) { + state.accounts = {}; + } state.accounts[userId] = this.createAccount(); const diskAccount = await this.getAccountFromDisk({ userId: userId }); state.accounts[userId].profile = diskAccount.profile; @@ -494,11 +499,11 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getCryptoMasterKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.cryptoMasterKey; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return account?.keys?.cryptoMasterKey; } async setCryptoMasterKey(value: SymmetricCryptoKey, options?: StorageOptions): Promise { @@ -604,23 +609,6 @@ export class StateService< await this.saveSecureStorageKey(partialKeys.biometricKey, value, options); } - async getDecodedToken(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.decodedToken; - } - - async setDecodedToken(value: any, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()) - ); - account.tokens.decodedToken = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()) - ); - } - @withPrototypeForArrayMembers(CipherView, CipherView.fromJSON) async getDecryptedCiphers(options?: StorageOptions): Promise { return ( @@ -657,11 +645,11 @@ export class StateService< ); } - @withPrototype(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedCryptoSymmetricKey(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.cryptoSymmetricKey?.decrypted; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return account?.keys?.cryptoSymmetricKey?.decrypted; } async setDecryptedCryptoSymmetricKey( @@ -678,14 +666,13 @@ export class StateService< ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedOrganizationKeys( options?: StorageOptions ): Promise> { const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - return account?.keys?.organizationKeys?.decrypted; + return this.recordToMap(account?.keys?.organizationKeys?.decrypted); } async setDecryptedOrganizationKeys( @@ -695,7 +682,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.organizationKeys.decrypted = value; + account.keys.organizationKeys.decrypted = this.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -725,7 +712,6 @@ export class StateService< ); } - @withPrototype(EncString) async getDecryptedPinProtected(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) @@ -762,14 +748,9 @@ export class StateService< } async getDecryptedPrivateKey(options?: StorageOptions): Promise { - const privateKey = ( + return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.privateKey; - let result = privateKey?.decrypted; - if (result == null && privateKey?.decryptedSerialized != null) { - result = Utils.fromByteStringToArray(privateKey.decryptedSerialized); - } - return result; + )?.keys?.privateKey.decrypted; } async setDecryptedPrivateKey(value: ArrayBuffer, options?: StorageOptions): Promise { @@ -777,21 +758,19 @@ export class StateService< this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.keys.privateKey.decrypted = value; - account.keys.privateKey.decryptedSerialized = - value == null ? null : Utils.fromBufferToByteString(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); } - @withPrototypeForMap(SymmetricCryptoKey, SymmetricCryptoKey.fromJSON) async getDecryptedProviderKeys( options?: StorageOptions ): Promise> { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.keys?.providerKeys?.decrypted; + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()) + ); + return this.recordToMap(account?.keys?.providerKeys?.decrypted); } async setDecryptedProviderKeys( @@ -801,7 +780,7 @@ export class StateService< const account = await this.getAccount( this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); - account.keys.providerKeys.decrypted = value; + account.keys.providerKeys.decrypted = this.mapToRecord(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -1538,7 +1517,6 @@ export class StateService< ); } - @withPrototype(EnvironmentUrls) async getEnvironmentUrls(options?: StorageOptions): Promise { if ((await this.state())?.activeUserId == null) { return await this.getGlobalEnvironmentUrls(options); @@ -2021,11 +1999,7 @@ export class StateService< const keys = ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) )?.keys; - let result = keys?.publicKey; - if (result == null && keys?.publicKeySerialized != null) { - result = Utils.fromByteStringToArray(keys.publicKeySerialized); - } - return result; + return keys?.publicKey; } async setPublicKey(value: ArrayBuffer, options?: StorageOptions): Promise { @@ -2033,7 +2007,6 @@ export class StateService< this.reconcileOptions(options, await this.defaultInMemoryOptions()) ); account.keys.publicKey = value; - account.keys.publicKeySerialized = value == null ? null : Utils.fromBufferToByteString(value); await this.saveAccount( account, this.reconcileOptions(options, await this.defaultInMemoryOptions()) @@ -2741,8 +2714,11 @@ export class StateService< : await this.secureStorageService.save(`${options.userId}${key}`, value, options); } - protected state(): Promise> { - return this.memoryStorageService.get>(keys.state); + protected async state(): Promise> { + const state = await this.memoryStorageService.get>(keys.state, { + deserializer: (s) => State.fromJSON(s), + }); + return state; } private async setState(state: State): Promise { @@ -2761,6 +2737,14 @@ export class StateService< await this.setState(updatedState); }); } + + private mapToRecord(map: Map): Record { + return map == null ? null : Object.fromEntries(map); + } + + private recordToMap(record: Record): Map { + return record == null ? null : new Map(Object.entries(record)); + } } export function withPrototype( @@ -2893,52 +2877,3 @@ function withPrototypeForObjectValues( }; }; } - -function withPrototypeForMap( - valuesConstructor: new (...args: any[]) => T, - valuesConverter: (input: any) => T = (i) => i -): ( - target: any, - propertyKey: string | symbol, - descriptor: PropertyDescriptor -) => { value: (...args: any[]) => Promise> } { - return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => { - const originalMethod = descriptor.value; - - return { - value: function (...args: any[]) { - const originalResult: Promise = originalMethod.apply(this, args); - - if (!(originalResult instanceof Promise)) { - throw new Error( - `Error applying prototype to stored value -- result is not a promise for method ${String( - propertyKey - )}` - ); - } - - return originalResult.then((result) => { - if (result == null) { - return null; - } else if (result instanceof Map) { - return result; - } else { - for (const key in Object.keys(result)) { - result[key] = - result[key] == null || - result[key].constructor.name === valuesConstructor.prototype.constructor.name - ? valuesConverter(result[key]) - : valuesConverter( - Object.create( - valuesConstructor.prototype, - Object.getOwnPropertyDescriptors(result[key]) - ) - ); - } - return new Map(Object.entries(result)); - } - }); - }, - }; - }; -} diff --git a/libs/common/src/services/stateMigration.service.ts b/libs/common/src/services/stateMigration.service.ts index 359b4acec46..e936860657a 100644 --- a/libs/common/src/services/stateMigration.service.ts +++ b/libs/common/src/services/stateMigration.service.ts @@ -12,7 +12,13 @@ import { OrganizationData } from "../models/data/organizationData"; import { PolicyData } from "../models/data/policyData"; import { ProviderData } from "../models/data/providerData"; import { SendData } from "../models/data/sendData"; -import { Account, AccountSettings, AccountSettingsSettings } from "../models/domain/account"; +import { + Account, + AccountSettings, + AccountSettingsSettings, + EncryptionPair, +} from "../models/domain/account"; +import { EncString } from "../models/domain/encString"; import { EnvironmentUrls } from "../models/domain/environmentUrls"; import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory"; import { GlobalState } from "../models/domain/globalState"; @@ -314,10 +320,10 @@ export class StateMigrationService< passwordGenerationOptions: (await this.get(v1Keys.passwordGenerationOptions)) ?? defaultAccount.settings.passwordGenerationOptions, - pinProtected: { + pinProtected: Object.assign(new EncryptionPair(), { decrypted: null, encrypted: await this.get(v1Keys.pinProtected), - }, + }), protectedPin: await this.get(v1Keys.protectedPin), settings: userId == null diff --git a/libs/common/src/services/token.service.ts b/libs/common/src/services/token.service.ts index 0922fa87518..5b6ffe6d889 100644 --- a/libs/common/src/services/token.service.ts +++ b/libs/common/src/services/token.service.ts @@ -93,11 +93,6 @@ export class TokenService implements TokenServiceAbstraction { // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js async decodeToken(token?: string): Promise { - const storedToken = await this.stateService.getDecodedToken(); - if (token === null && storedToken != null) { - return storedToken; - } - token = token ?? (await this.stateService.getAccessToken()); if (token == null) { diff --git a/libs/common/src/types/deep-jsonify.ts b/libs/common/src/types/deep-jsonify.ts new file mode 100644 index 00000000000..a83b8e4549f --- /dev/null +++ b/libs/common/src/types/deep-jsonify.ts @@ -0,0 +1,44 @@ +import { + PositiveInfinity, + NegativeInfinity, + JsonPrimitive, + TypedArray, + JsonValue, +} from "type-fest"; +import { NotJsonable } from "type-fest/source/jsonify"; + +/** + * Extracted from type-fest and extended with Jsonification of objects returned from `toJSON` methods. + */ +export type DeepJsonify = + // Check if there are any non-JSONable types represented in the union. + // Note: The use of tuples in this first condition side-steps distributive conditional types + // (see https://github.com/microsoft/TypeScript/issues/29368#issuecomment-453529532) + [Extract] extends [never] + ? T extends PositiveInfinity | NegativeInfinity ? null + : T extends JsonPrimitive + ? T // Primitive is acceptable + : T extends number ? number + : T extends string ? string + : T extends boolean ? boolean + : T extends Map | Set ? Record // {} + : T extends TypedArray ? Record + : T extends Array + ? Array> // It's an array: recursive call for its children + : T extends object + ? T extends { toJSON(): infer J } + ? (() => J) extends () => JsonValue // Is J assignable to JsonValue? + ? J // Then T is Jsonable and its Jsonable value is J + : {[P in keyof J as P extends symbol + ? never + : J[P] extends NotJsonable + ? never + : P]: DeepJsonify[P]>; + } // Not Jsonable because its toJSON() method does not return JsonValue + : {[P in keyof T as P extends symbol + ? never + : T[P] extends NotJsonable + ? never + : P]: DeepJsonify[P]>} // It's an object: recursive call for its children + : never // Otherwise any other non-object is removed + : never; // Otherwise non-JSONable type union was found not empty From 9b1b5000114f48c99c954c95cad49da5cd815522 Mon Sep 17 00:00:00 2001 From: mimartin12 <77340197+mimartin12@users.noreply.github.com> Date: Thu, 22 Sep 2022 08:34:08 -0600 Subject: [PATCH 09/23] fix secret pull (#3591) --- .github/workflows/release-qa-web.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-qa-web.yml b/.github/workflows/release-qa-web.yml index 80d15ac653d..d6cafc274bb 100644 --- a/.github/workflows/release-qa-web.yml +++ b/.github/workflows/release-qa-web.yml @@ -45,8 +45,7 @@ jobs: id: retrieve-secrets env: KEYVAULT: bitwarden-qa-kv - SECRETS: | - qa-aks-kubectl-credentials + SECRETS: "qa-aks-kubectl-credentials" run: | for i in ${SECRETS//,/ } do @@ -56,9 +55,9 @@ jobs: done - name: Login with qa-aks-kubectl-credentials SP - uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 + uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 with: - creds: ${{ env.qa-aks-kubectl-credentials }} + creds: ${{ steps.retrieve-secrets.outputs.qa-aks-kubectl-credentials }} - name: Setup AKS access run: | From ae085302763945d8c2dbae8ab7168f29099b4532 Mon Sep 17 00:00:00 2001 From: mimartin12 <77340197+mimartin12@users.noreply.github.com> Date: Thu, 22 Sep 2022 09:46:17 -0600 Subject: [PATCH 10/23] [DEVOPS-933] - Migrate QA Web to cloudflare pages (#3569) --- .github/workflows/release-qa-web.yml | 83 +--------------------------- 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/.github/workflows/release-qa-web.yml b/.github/workflows/release-qa-web.yml index d6cafc274bb..1e31c95cbeb 100644 --- a/.github/workflows/release-qa-web.yml +++ b/.github/workflows/release-qa-web.yml @@ -2,93 +2,12 @@ name: QA - Web Release on: - workflow_dispatch: - inputs: - image_extension: - description: "Image tag extension" - required: false - azure_publish: - description: 'Release to Azure' - required: false - default: true - type: boolean - cloudflare_publish: - description: 'Release to Cloudflare' - required: false - default: true - type: boolean - -env: - _QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa" - _QA_CLUSTER_NAME: "bw-aks-qa" - _QA_K8S_NAMESPACE: "bw-qa" - _QA_K8S_APP_NAME: "bw-web" + workflow_dispatch: {} jobs: - deploy: - name: Deploy QA Web - if: inputs.azure_publish - runs-on: ubuntu-20.04 - steps: - - name: Checkout Repo - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 - - - name: Setup - run: export PATH=$PATH:~/work/web/web - - - name: Login to Azure - uses: Azure/login@ec3c14589bd3e9312b3cc8c41e6860e258df9010 # v1.1 - with: - creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }} - - - name: Retrieve secrets - id: retrieve-secrets - env: - KEYVAULT: bitwarden-qa-kv - SECRETS: "qa-aks-kubectl-credentials" - run: | - for i in ${SECRETS//,/ } - do - VALUE=$(az keyvault secret show --vault-name $KEYVAULT --name $i --query value --output tsv) - echo "::add-mask::$VALUE" - echo "::set-output name=$i::$VALUE" - done - - - name: Login with qa-aks-kubectl-credentials SP - uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 - with: - creds: ${{ steps.retrieve-secrets.outputs.qa-aks-kubectl-credentials }} - - - name: Setup AKS access - run: | - echo "---az install---" - az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin - echo "---az get-creds---" - az aks get-credentials -n $_QA_CLUSTER_NAME -g $_QA_CLUSTER_RESOURCE_GROUP - - - name: Get image tag - id: image_tag - run: | - IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") - TAG_EXTENSION=${{ github.event.inputs.image_extension }} - - if [[ $TAG_EXTENSION ]]; then - IMAGE_TAG=$IMAGE_TAG-$TAG_EXTENSION - fi - echo "::set-output name=value::$IMAGE_TAG" - - - name: Deploy Web image - env: - IMAGE_TAG: ${{ steps.image_tag.outputs.value }} - run: | - kubectl set image -n $_QA_K8S_NAMESPACE deployment/web web=bitwardenqa.azurecr.io/web:$IMAGE_TAG --record - kubectl rollout restart -n $_QA_K8S_NAMESPACE deployment/web - kubectl rollout status deployment/web -n $_QA_K8S_NAMESPACE - cfpages-deploy: name: Deploy Web Vault to QA CloudFlare Pages branch runs-on: ubuntu-20.04 - if: inputs.cloudflare_publish steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@1b599fe41a0ef1f95191e7f2eec4743f2d7dfc48 From 32c91deb36024cdc525cde3ee23db37f45369a69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Thu, 22 Sep 2022 18:01:34 +0200 Subject: [PATCH 11/23] Add stub for ephemeral env web build workflow (#3593) --- .github/workflows/build-web-ee.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/build-web-ee.yml diff --git a/.github/workflows/build-web-ee.yml b/.github/workflows/build-web-ee.yml new file mode 100644 index 00000000000..678ccd83200 --- /dev/null +++ b/.github/workflows/build-web-ee.yml @@ -0,0 +1,16 @@ +--- +name: Build Web for EE + +on: + workflow_dispatch: + +jobs: + stub: + name: stub + runs-on: ubuntu-20.04 + steps: + - name: Checkout repo + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 + + - name: Stub + run: print 'This is only a stub' From a8f54f1c4592b6366c219b8435bfa761205aae9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:02:25 +0200 Subject: [PATCH 12/23] Autosync the updated translations (#3597) Co-authored-by: github-actions <> --- apps/web/src/locales/be/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index a140a225414..d8f2faff0c0 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -3809,7 +3809,7 @@ "message": "Пратэрмінавана" }, "searchSends": { - "message": "Пошук у Send'ах", + "message": "Пошук Send'аў", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPassword": { From 32eac70c82c6f65c5c78ad40c1f2f885e7ac7acc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 23 Sep 2022 02:02:54 +0200 Subject: [PATCH 13/23] Autosync the updated translations (#3598) Co-authored-by: github-actions <> --- apps/desktop/src/locales/it/messages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index 5e139658e11..17de59f491f 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -1461,7 +1461,7 @@ "message": "Una cassaforte bloccata richiede l'inserimento della password principale per accedere nuovamente." }, "vaultTimeoutActionLogOutDesc": { - "message": "La disconnessione dalla cassaforte richiede l'inserimento della password principale per accedere nuovamente." + "message": "Per accedere nuovamente alla cassaforte è necessaria una nuova autenticazione." }, "lock": { "message": "Blocca", @@ -1565,7 +1565,7 @@ "message": "I termini di servizio e l'informativa sulla privacy non sono stati accettati." }, "enableBrowserIntegration": { - "message": "Abilita l'integrazione con il browser" + "message": "Abilita l'integrazione del browser" }, "enableBrowserIntegrationDesc": { "message": "L'integrazione del browser è utilizzata per l'autenticazione biometrica." @@ -1995,6 +1995,6 @@ "message": "Mir" }, "vault": { - "message": "Vault" + "message": "Cassaforte" } } From f4e61d1ceccb566e48ceb28968c931737c65ceb6 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Fri, 23 Sep 2022 15:47:17 -0400 Subject: [PATCH 14/23] [SG-520] Native messaging handler (#3566) * [SG-523] Base test runner app for native messages (#3269) * Base test runner app for native messages * Remove default test script * Add case for canceled status * Modify to allow usage of libs crypto services and functions * Small adjustments * Handshake request (#3277) * Handshake request * Fix capitalization * Update info text * lock node-ipc to 9.2.1 * [SG-569] Native Messaging settings bug (#3285) * Fix bug where updating setting wasn't starting the native messaging listener * Update test runner error message * [SG-532] Implement Status command in Native Messaging Service (#3310) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Remove in progress file (merge error) * Move models to their own folder and add index.ts * Remove file that got un-deleted * Remove file that will be added in separate command * Fix imports that got borked * [SG-533] Implement bw-credential-retrieval (#3334) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * Add error handling for passing a bad public key to handshake * [SG-534] and [SG-535] Implement Credential Create and Update commands (#3342) * Status command start * Refactor ipc test service and add status command * fixed linter errors * Move types into a model file * Cleanup and comments * Fix auth status condition * Remove .vscode settings file. Fix this in a separate work item * Implement bw-credential-retrieval * Add active field to status response * Add bw-credential-create * Better response handling in test runner * Extract native messaging types into their own files * Remove experimental decorators * Turn off no console lint rule for the test runner * Casing fix * Models import casing fixes * bw-cipher-create move type into its own file * Use LogUtils for all logging * Implement bw-credential-update * Give naming conventions for types * Rename file correctly * Update handleEncyptedMessage with EncString changes * [SG-626] Fix Desktop app not showing updated credentials from native messages (#3380) * Add MessagingService to send messages on login create and update * Add `not-active-user` error to create and update and other refactors * [SG-536] Implement bw-generate-password (#3370) * implement bw-generate-password * Fix merge conflict resolution errors * Update apps/desktop/native-messaging-test-runner/src/bw-generate-password.ts Co-authored-by: Addison Beck * Logging improvements * Add NativeMessagingVersion enum * Add version check in NativeMessagingHandler Co-authored-by: Addison Beck * Refactor account status checks and check for locked state in generate command (#3461) * Add feawture flag to show/hide ddg setting (#3506) * [SG-649] Add confirmation dialog and tweak shared key retrieval (#3451) * Add confirmation dialog when completing handshake * Copy updates for dialog * HandshakeResponse type fixes * Add longer timeout for handshake command * [SG-663] RefactorNativeMessagingHandlerService and strengthen typing (#3551) * NativeMessageHandlerService refactor and additional types * Return empty array if no uri to retrieve command * Move commands from test runner into a separate folder * Fix bug where confirmation dialog messes with styling * Enable DDG feature * Fix generated password not saving to history * Take credentialId as parameter to update * Add applicationName to handshake payload * Add warning text to confirmation modal Co-authored-by: Addison Beck --- apps/desktop/config/development.json | 4 +- apps/desktop/config/production.json | 4 +- .../.eslintrc.json | 5 + .../package-lock.json | 681 ++++++++++++++++++ .../native-messaging-test-runner/package.json | 35 + .../src/commands/bw-credential-create.ts | 66 ++ .../src/commands/bw-credential-retrieval.ts | 46 ++ .../src/commands/bw-credential-update.ts | 89 +++ .../src/commands/bw-generate-password.ts | 46 ++ .../src/commands/bw-handshake.ts | 25 + .../src/commands/bw-status.ts | 29 + .../src/deferred.ts | 26 + .../src/ipcService.ts | 166 +++++ .../src/logUtils.ts | 29 + .../src/nativeMessageService.ts | 235 ++++++ .../native-messaging-test-runner/src/race.ts | 25 + .../src/variables.ts | 27 + .../tsconfig.json | 17 + .../src/app/accounts/settings.component.html | 17 + .../src/app/accounts/settings.component.ts | 24 + .../src/app/services/services.module.ts | 28 + apps/desktop/src/flags.ts | 4 +- apps/desktop/src/locales/en/messages.json | 27 +- apps/desktop/src/main.ts | 5 +- apps/desktop/src/main/messaging.main.ts | 8 + apps/desktop/src/main/nativeMessaging.main.ts | 36 + .../nativeMessaging/decryptedCommandData.ts | 6 + .../nativeMessaging/encryptedCommand.ts | 6 + .../nativeMessaging/encryptedMessage.ts | 8 + .../credentialCreatePayload.ts | 7 + .../credentialRetrievePayload.ts | 4 + .../credentialUpdatePayload.ts | 8 + .../passwordGeneratePayload.ts | 3 + .../encryptedMessageResponse.ts | 7 + .../accountStatusResponse.ts | 6 + .../cannotDecryptErrorResponse.ts | 3 + .../cipherResponse.ts | 7 + .../encryptedMessageResponse.ts | 16 + .../failureStatusResponse.ts | 3 + .../generateResponse.ts | 3 + .../successStatusResponse.ts | 3 + .../userStatusErrorResponse.ts | 3 + .../src/models/nativeMessaging/index.ts | 25 + .../models/nativeMessaging/legacyMessage.ts | 8 + .../nativeMessaging/legacyMessageWrapper.ts | 8 + .../src/models/nativeMessaging/message.ts | 4 + .../models/nativeMessaging/messageCommon.ts | 4 + .../nativeMessaging/unencryptedCommand.ts | 1 + .../nativeMessaging/unencryptedMessage.ts | 10 + .../unencryptedMessageResponse.ts | 16 + .../encryptedMessageHandlerService.ts | 228 ++++++ .../services/nativeMessageHandler.service.ts | 221 ++++++ .../src/services/nativeMessaging.service.ts | 38 +- libs/common/src/abstractions/state.service.ts | 7 + .../src/enums/nativeMessagingVersion.ts | 4 + libs/common/src/models/domain/globalState.ts | 1 + libs/common/src/services/state.service.ts | 41 ++ 57 files changed, 2386 insertions(+), 27 deletions(-) create mode 100644 apps/desktop/native-messaging-test-runner/.eslintrc.json create mode 100644 apps/desktop/native-messaging-test-runner/package-lock.json create mode 100644 apps/desktop/native-messaging-test-runner/package.json create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/deferred.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/ipcService.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/logUtils.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/race.ts create mode 100644 apps/desktop/native-messaging-test-runner/src/variables.ts create mode 100644 apps/desktop/native-messaging-test-runner/tsconfig.json create mode 100644 apps/desktop/src/models/nativeMessaging/decryptedCommandData.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedCommand.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessage.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialRetrievePayload.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessagePayloads/passwordGeneratePayload.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/accountStatusResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/cannotDecryptErrorResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/cipherResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/encryptedMessageResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/failureStatusResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/generateResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/successStatusResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/encryptedMessageResponses/userStatusErrorResponse.ts create mode 100644 apps/desktop/src/models/nativeMessaging/index.ts create mode 100644 apps/desktop/src/models/nativeMessaging/legacyMessage.ts create mode 100644 apps/desktop/src/models/nativeMessaging/legacyMessageWrapper.ts create mode 100644 apps/desktop/src/models/nativeMessaging/message.ts create mode 100644 apps/desktop/src/models/nativeMessaging/messageCommon.ts create mode 100644 apps/desktop/src/models/nativeMessaging/unencryptedCommand.ts create mode 100644 apps/desktop/src/models/nativeMessaging/unencryptedMessage.ts create mode 100644 apps/desktop/src/models/nativeMessaging/unencryptedMessageResponse.ts create mode 100644 apps/desktop/src/services/encryptedMessageHandlerService.ts create mode 100644 apps/desktop/src/services/nativeMessageHandler.service.ts create mode 100644 libs/common/src/enums/nativeMessagingVersion.ts diff --git a/apps/desktop/config/development.json b/apps/desktop/config/development.json index 3c93018e65f..b587e9ecfb9 100644 --- a/apps/desktop/config/development.json +++ b/apps/desktop/config/development.json @@ -1,4 +1,6 @@ { "devFlags": {}, - "flags": {} + "flags": { + "showDDGSetting": true + } } diff --git a/apps/desktop/config/production.json b/apps/desktop/config/production.json index b04d1531a2f..56f19341304 100644 --- a/apps/desktop/config/production.json +++ b/apps/desktop/config/production.json @@ -1,3 +1,5 @@ { - "flags": {} + "flags": { + "showDDGSetting": true + } } diff --git a/apps/desktop/native-messaging-test-runner/.eslintrc.json b/apps/desktop/native-messaging-test-runner/.eslintrc.json new file mode 100644 index 00000000000..d5ba8f9d9ca --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-console": "off" + } +} diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json new file mode 100644 index 00000000000..34b1bb54068 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -0,0 +1,681 @@ +{ + "name": "native-messaging-test-runner", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "native-messaging-test-runner", + "version": "1.0.0", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/node": "file:../../../libs/node", + "module-alias": "^2.2.2", + "node-ipc": "9.2.1", + "ts-node": "^10.9.1", + "uuid": "^8.3.2", + "yargs": "^17.5.1" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.6.5", + "@types/node-ipc": "9.2.0", + "typescript": "^4.7.4" + } + }, + "../../../libs/common": { + "name": "@bitwarden/common", + "version": "0.0.0", + "license": "GPL-3.0" + }, + "../../../libs/node": { + "name": "@bitwarden/node", + "version": "0.0.0", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../common" + } + }, + "node_modules/@bitwarden/common": { + "resolved": "../../../libs/common", + "link": true + }, + "node_modules/@bitwarden/node": { + "resolved": "../../../libs/node", + "link": true + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.6.5", + "license": "MIT" + }, + "node_modules/@types/node-ipc": { + "version": "9.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.8.0", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/js-queue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "dependencies": { + "easy-stack": "^1.0.1" + }, + "engines": { + "node": ">=1.0.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "license": "ISC" + }, + "node_modules/module-alias": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + }, + "node_modules/node-ipc": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", + "dependencies": { + "event-pubsub": "4.3.0", + "js-message": "1.0.7", + "js-queue": "2.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@bitwarden/common": { + "version": "file:../../../libs/common" + }, + "@bitwarden/node": { + "version": "file:../../../libs/node", + "requires": { + "@bitwarden/common": "file:../common" + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0" + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.9" + }, + "@tsconfig/node12": { + "version": "1.0.11" + }, + "@tsconfig/node14": { + "version": "1.0.3" + }, + "@tsconfig/node16": { + "version": "1.0.3" + }, + "@types/node": { + "version": "18.6.5" + }, + "@types/node-ipc": { + "version": "9.2.0", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "8.8.0" + }, + "acorn-walk": { + "version": "8.2.0" + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "arg": { + "version": "4.1.3" + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "create-require": { + "version": "1.1.1" + }, + "diff": { + "version": "4.0.2" + }, + "easy-stack": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz", + "integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, + "event-pubsub": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz", + "integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "js-message": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==", + "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz", + "integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==" + }, + "js-queue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz", + "integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==", + "requires": { + "easy-stack": "^1.0.1" + } + }, + "make-error": { + "version": "1.3.6" + }, + "module-alias": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/module-alias/-/module-alias-2.2.2.tgz", + "integrity": "sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==" + }, + "node-ipc": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz", + "integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==", + "requires": { + "event-pubsub": "4.3.0", + "js-message": "1.0.7", + "js-queue": "2.0.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "ts-node": { + "version": "10.9.1", + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "4.7.4" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yn": { + "version": "3.1.1" + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json new file mode 100644 index 00000000000..85d4a1ab6a6 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -0,0 +1,35 @@ +{ + "name": "native-messaging-test-runner", + "version": "1.0.0", + "description": "Test runner for Desktop native messaging", + "main": "dist/bw-handshake.ts", + "scripts": { + "handshake": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.js", + "status": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-status.js", + "retrieve": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.js", + "create": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.js", + "update": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.js", + "generate": "tsc && node dist/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.js" + }, + "author": "Bitwarden Inc. (https://bitwarden.com)", + "license": "GPL-3.0", + "dependencies": { + "@bitwarden/common": "file:../../../libs/common", + "@bitwarden/node": "file:../../../libs/node", + "module-alias": "^2.2.2", + "node-ipc": "9.2.1", + "ts-node": "^10.9.1", + "uuid": "^8.3.2", + "yargs": "^17.5.1" + }, + "devDependencies": { + "@tsconfig/node16": "^1.0.3", + "@types/node": "^18.6.5", + "@types/node-ipc": "9.2.0", + "typescript": "^4.7.4" + }, + "_moduleAliases": { + "@bitwarden/common": "dist/libs/common/src", + "@bitwarden/node/services/nodeCryptoFunction.service": "dist/libs/node/src/services/nodeCryptoFunction.service" + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts new file mode 100644 index 00000000000..a017b4c72cb --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-create.ts @@ -0,0 +1,66 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { CredentialCreatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload"; +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("name", { + alias: "n", + demand: true, + describe: "Name that the created login will be given", + type: "string", +}).argv; + +const { name } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + // Get active account userId + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0]; + if (activeUser === undefined) { + LogUtils.logError("No active or unlocked user"); + } + LogUtils.logInfo("Active userId: " + activeUser.id); + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.credentialCreation(handshakeResponse.sharedKey, { + name: name, + userName: "SuperAwesomeUser", + password: "dolhpin", + uri: "google.com", + userId: activeUser.id, + } as CredentialCreatePayload); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else if (response.payload.error === "locked") { + LogUtils.logError("Error: vault is locked"); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts new file mode 100644 index 00000000000..17244623b55 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-retrieval.ts @@ -0,0 +1,46 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("uri", { + alias: "u", + demand: true, + describe: "The uri to retrieve logins for", + type: "string", +}).argv; + +const { uri } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.credentialRetrieval(handshakeResponse.sharedKey, uri); + + if (response.payload.error != null) { + LogUtils.logError("Error response returned: ", response.payload.error); + } else { + LogUtils.logSuccess("Credentials returned ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts new file mode 100644 index 00000000000..ecfbd3f5bb8 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-credential-update.ts @@ -0,0 +1,89 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { CredentialUpdatePayload } from "../../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload"; +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +// Command line arguments +const argv: any = yargs(hideBin(process.argv)) + .option("name", { + alias: "n", + demand: true, + describe: "Name that the updated login will be given", + type: "string", + }) + .option("username", { + alias: "u", + demand: true, + describe: "Username that the login will be given", + type: "string", + }) + .option("password", { + alias: "p", + demand: true, + describe: "Password that the login will be given", + type: "string", + }) + .option("uri", { + demand: true, + describe: "Uri that the login will be given", + type: "string", + }) + .option("credentialId", { + demand: true, + describe: "GUID of the credential to update", + type: "string", + }).argv; + +const { name, username, password, uri, credentialId } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + LogUtils.logSuccess("Handshake success response"); + + // Get active account userId + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + const activeUser = status.payload.filter((a) => a.active === true && a.status === "unlocked")[0]; + if (activeUser === undefined) { + LogUtils.logError("No active or unlocked user"); + } + LogUtils.logInfo("Active userId: " + activeUser.id); + + const response = await nativeMessageService.credentialUpdate(handshakeResponse.sharedKey, { + name: name, + password: password, + userName: username, + uri: uri, + userId: activeUser.id, + credentialId: credentialId, + } as CredentialUpdatePayload); + + if (response.payload.status === "failure") { + LogUtils.logError("Failure response returned "); + } else if (response.payload.status === "success") { + LogUtils.logSuccess("Success response returned "); + } else { + LogUtils.logWarning("Other response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts new file mode 100644 index 00000000000..34bb41abb95 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-generate-password.ts @@ -0,0 +1,46 @@ +import "module-alias/register"; + +import yargs from "yargs"; +import { hideBin } from "yargs/helpers"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +const argv: any = yargs(hideBin(process.argv)).option("userId", { + alias: "u", + demand: true, + describe: "UserId to generate password for", + type: "string", +}).argv; + +const { userId } = argv; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + // Handshake + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + + LogUtils.logSuccess("Handshake success response"); + const response = await nativeMessageService.generatePassword(handshakeResponse.sharedKey, userId); + + if (response.payload.error != null) { + LogUtils.logError("Error response returned: ", response.payload.error); + } else { + LogUtils.logSuccess("Response: ", response); + } + + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts new file mode 100644 index 00000000000..f3098062c46 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-handshake.ts @@ -0,0 +1,25 @@ +import "module-alias/register"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + + const response = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + LogUtils.logSuccess("Received response to handshake request"); + if (response.status) { + LogUtils.logSuccess("Handshake success response"); + } else if (response.error === "canceled") { + LogUtils.logWarning("Handshake canceled by user"); + } else { + LogUtils.logError("Handshake failure response"); + } + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts b/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts new file mode 100644 index 00000000000..7782e203cb8 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/commands/bw-status.ts @@ -0,0 +1,29 @@ +import "module-alias/register"; + +import { NativeMessagingVersion } from "@bitwarden/common/enums/nativeMessagingVersion"; + +import { LogUtils } from "../logUtils"; +import NativeMessageService from "../nativeMessageService"; +import * as config from "../variables"; + +(async () => { + const nativeMessageService = new NativeMessageService(NativeMessagingVersion.One); + + LogUtils.logInfo("Sending Handshake"); + const handshakeResponse = await nativeMessageService.sendHandshake( + config.testRsaPublicKey, + config.applicationName + ); + LogUtils.logSuccess("Received response to handshake request"); + + if (!handshakeResponse.status) { + LogUtils.logError(" Handshake failed. Error was: " + handshakeResponse.error); + nativeMessageService.disconnect(); + return; + } + LogUtils.logSuccess("Handshake success response"); + const status = await nativeMessageService.checkStatus(handshakeResponse.sharedKey); + + LogUtils.logSuccess("Status output is: ", status); + nativeMessageService.disconnect(); +})(); diff --git a/apps/desktop/native-messaging-test-runner/src/deferred.ts b/apps/desktop/native-messaging-test-runner/src/deferred.ts new file mode 100644 index 00000000000..b6478bcf268 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/deferred.ts @@ -0,0 +1,26 @@ +// Wrapper for a promise that we can await the promise in one case +// while allowing an unrelated event to fulfill it elsewhere. +export default class Deferred { + private promise: Promise; + private resolver: (T?) => void; + private rejecter: (Error?) => void; + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolver = resolve; + this.rejecter = reject; + }); + } + + resolve(value?: T) { + this.resolver(value); + } + + reject(error?: Error) { + this.rejecter(error); + } + + getPromise(): Promise { + return this.promise; + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/ipcService.ts b/apps/desktop/native-messaging-test-runner/src/ipcService.ts new file mode 100644 index 00000000000..eadc69a3513 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/ipcService.ts @@ -0,0 +1,166 @@ +import { homedir } from "os"; + +import * as NodeIPC from "node-ipc"; + +import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon"; +import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse"; + +import Deferred from "./deferred"; +import { race } from "./race"; + +NodeIPC.config.id = "native-messaging-test-runner"; +NodeIPC.config.maxRetries = 0; +NodeIPC.config.silent = true; + +const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`; +const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds + +export type MessageHandler = (MessageCommon) => void; + +export enum IPCConnectionState { + Disconnected = "disconnected", + Connecting = "connecting", + Connected = "connected", +} + +export type IPCOptions = { + overrideTimeout?: number; +}; + +export default class IPCService { + // The current connection state of the socket. + private connectionState: IPCConnectionState = IPCConnectionState.Disconnected; + + // Messages that have been sent, but have not yet received responses + private pendingMessages = new Map>(); + + // A set of deferred promises that are awaiting socket connection + private awaitingConnection = new Set>(); + + constructor(private socketName: string, private messageHandler: MessageHandler) {} + + async connect(): Promise { + console.log("[IPCService] connecting..."); + if (this.connectionState === IPCConnectionState.Connected) { + // Socket is already connected. Don't throw, just allow the callsite to proceed + return; + } + + const deferredConnections = new Deferred(); + + this.awaitingConnection.add(deferredConnections); + + // If the current connection state is disconnected, we should start trying to connect. + // The only other possible connection state at this point is "connecting" and if this + // is the case, we just want to add a deferred promise to the awaitingConnection collection + // and not try to initiate the connection again. + if (this.connectionState === IPCConnectionState.Disconnected) { + this._connect(); + } + + return deferredConnections.getPromise(); + } + + private _connect() { + this.connectionState = IPCConnectionState.Connecting; + + NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => { + // Process incoming message + this.getSocket().on("message", (message: any) => { + this.processMessage(message); + }); + + this.getSocket().on("error", (error: Error) => { + // Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be + // invoked multiple times each time a connection error happens + console.log("[IPCService] errored"); + console.log( + "\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m" + ); + this.awaitingConnection.forEach((deferred) => { + console.log(`rejecting: ${deferred}`); + deferred.reject(error); + }); + this.awaitingConnection.clear(); + }); + + this.getSocket().on("connect", () => { + console.log("[IPCService] connected"); + this.connectionState = IPCConnectionState.Connected; + + this.awaitingConnection.forEach((deferred) => { + deferred.resolve(null); + }); + this.awaitingConnection.clear(); + }); + + this.getSocket().on("disconnect", () => { + console.log("[IPCService] disconnected"); + this.connectionState = IPCConnectionState.Disconnected; + }); + }); + } + + disconnect() { + console.log("[IPCService] disconnecting..."); + if (this.connectionState !== IPCConnectionState.Disconnected) { + NodeIPC.disconnect(this.socketName); + } + } + + async sendMessage( + message: MessageCommon, + options: IPCOptions = {} + ): Promise { + console.log("[IPCService] sendMessage"); + if (this.pendingMessages.has(message.messageId)) { + throw new Error(`A message with the id: ${message.messageId} has already been sent.`); + } + + // Creates a new deferred promise that allows us to convert a message received over the IPC socket + // into a response for a message that we previously sent. This mechanism relies on the fact that we + // create a unique message id and attach it with each message. Response messages are expected to + // include the message id of the message they are responding to. + const deferred = new Deferred(); + + this.pendingMessages.set(message.messageId, deferred); + + this.getSocket().emit("message", message); + + try { + // Since we can not guarentee that a response message will ever be sent, we put a timeout + // on messages + return race({ + promise: deferred.getPromise(), + timeout: options?.overrideTimeout ?? DEFAULT_MESSAGE_TIMEOUT, + error: new Error(`Message: ${message.messageId} timed out`), + }); + } catch (error) { + // If there is a timeout, remove the message from the pending messages set + // before triggering error handling elsewhere. + this.pendingMessages.delete(message.messageId); + throw error; + } + } + + private getSocket() { + return NodeIPC.of[this.socketName]; + } + + private processMessage(message: any) { + // If the message is a response to a previous message, resolve the deferred promise that + // is awaiting that response. Otherwise, assume this was a new message that wasn't sent as a + // response and invoke the message handler. + if (message.messageId && this.pendingMessages.has(message.messageId)) { + const deferred = this.pendingMessages.get(message.messageId); + + // In the future, this could be improved to add ability to reject, but most messages coming in are + // encrypted at this point so we're unable to determine if they contain error info. + deferred.resolve(message); + + this.pendingMessages.delete(message.messageId); + } else { + this.messageHandler(message); + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/logUtils.ts b/apps/desktop/native-messaging-test-runner/src/logUtils.ts new file mode 100644 index 00000000000..0e7c39742e3 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/logUtils.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ + +// Class for logging messages with colors for ease of reading important info +// Reference: https://stackoverflow.com/a/41407246 +export class LogUtils { + static logSuccess(message: string, payload?: any): void { + this.logFormat(message, "32", payload); + } + + static logWarning(message: string, payload?: any): void { + this.logFormat(message, "33", payload); + } + + static logError(message: string, payload?: any): void { + this.logFormat(message, "31", payload); + } + + static logInfo(message: string, payload?: any): void { + this.logFormat(message, "36", payload); + } + + private static logFormat(message: string, color: string, payload?: any) { + if (payload) { + console.log(`\x1b[${color}m ${message} \x1b[0m`, payload); + } else { + console.log(`\x1b[${color}m ${message} \x1b[0m`); + } + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts new file mode 100644 index 00000000000..ada09064e3c --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/nativeMessageService.ts @@ -0,0 +1,235 @@ +import "module-alias/register"; + +import { v4 as uuidv4 } from "uuid"; + +import { Utils } from "@bitwarden/common/misc/utils"; +import { EncString } from "@bitwarden/common/models/domain/encString"; +import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetricCryptoKey"; +import { ConsoleLogService } from "@bitwarden/common/services/consoleLog.service"; +import { EncryptService } from "@bitwarden/common/services/encrypt.service"; +import { NodeCryptoFunctionService } from "@bitwarden/node/services/nodeCryptoFunction.service"; + +import { DecryptedCommandData } from "../../src/models/nativeMessaging/decryptedCommandData"; +import { EncryptedMessage } from "../../src/models/nativeMessaging/encryptedMessage"; +import { CredentialCreatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialCreatePayload"; +import { CredentialUpdatePayload } from "../../src/models/nativeMessaging/encryptedMessagePayloads/credentialUpdatePayload"; +import { EncryptedMessageResponse } from "../../src/models/nativeMessaging/encryptedMessageResponse"; +import { MessageCommon } from "../../src/models/nativeMessaging/messageCommon"; +import { UnencryptedMessage } from "../../src/models/nativeMessaging/unencryptedMessage"; +import { UnencryptedMessageResponse } from "../../src/models/nativeMessaging/unencryptedMessageResponse"; + +import IPCService, { IPCOptions } from "./ipcService"; +import * as config from "./variables"; + +type HandshakeResponse = { + status: boolean; + sharedKey: string; + error?: "canceled" | "cannot-decrypt"; +}; + +const CONFIRMATION_MESSAGE_TIMEOUT = 100 * 1000; // 100 seconds + +export default class NativeMessageService { + private ipcService: IPCService; + private nodeCryptoFunctionService: NodeCryptoFunctionService; + private encryptService: EncryptService; + + constructor(private apiVersion: number) { + console.log("Starting native messaging service"); + this.ipcService = new IPCService(`bitwarden`, (rawMessage) => { + console.log(`Received unexpected: `, rawMessage); + }); + + this.nodeCryptoFunctionService = new NodeCryptoFunctionService(); + this.encryptService = new EncryptService( + this.nodeCryptoFunctionService, + new ConsoleLogService(false), + false + ); + } + + // Commands + + async sendHandshake(publicKey: string, applicationName: string): Promise { + const rawResponse = await this.sendUnencryptedMessage( + { + command: "bw-handshake", + payload: { + publicKey, + applicationName: applicationName, + }, + }, + { + overrideTimeout: CONFIRMATION_MESSAGE_TIMEOUT, + } + ); + return rawResponse.payload as HandshakeResponse; + } + + async checkStatus(key: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-status", + }, + key + ); + + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialRetrieval(key: string, uri: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-retrieval", + payload: { + uri: uri, + }, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialCreation( + key: string, + credentialData: CredentialCreatePayload + ): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-create", + payload: credentialData, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async credentialUpdate( + key: string, + credentialData: CredentialUpdatePayload + ): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-credential-update", + payload: credentialData, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + async generatePassword(key: string, userId: string): Promise { + const encryptedCommand = await this.encryptCommandData( + { + command: "bw-generate-password", + payload: { + userId: userId, + }, + }, + key + ); + const response = await this.sendEncryptedMessage({ + encryptedCommand, + }); + + return this.decryptResponsePayload(response.encryptedPayload, key); + } + + // Private message sending + + private async sendEncryptedMessage( + message: Omit, + options: IPCOptions = {} + ): Promise { + const result = await this.sendMessage(message, options); + return result as EncryptedMessageResponse; + } + + private async sendUnencryptedMessage( + message: Omit, + options: IPCOptions = {} + ): Promise { + const result = await this.sendMessage(message, options); + return result as UnencryptedMessageResponse; + } + + private async sendMessage( + message: + | Omit + | Omit, + options: IPCOptions + ): Promise { + // Attempt to connect before sending any messages. If the connection has already + // been made, this is a NOOP within the IPCService. + await this.ipcService.connect(); + + const commonFields: MessageCommon = { + // Create a messageId that can be used as a lookup when we get a response + messageId: uuidv4(), + version: this.apiVersion, + }; + const fullMessage: UnencryptedMessage | EncryptedMessage = { + ...message, + ...commonFields, + }; + + console.log(`[NativeMessageService] sendMessage with id: ${fullMessage.messageId}`); + + const response = await this.ipcService.sendMessage(fullMessage, options); + + console.log(`[NativeMessageService] received response for message: ${fullMessage.messageId}`); + + return response; + } + + disconnect() { + this.ipcService.disconnect(); + } + + // Data Encryption + private async encryptCommandData( + commandData: DecryptedCommandData, + key: string + ): Promise { + const commandDataString = JSON.stringify(commandData); + + const sharedKey = await this.getSharedKeyForKey(key); + + return this.encryptService.encrypt(commandDataString, sharedKey); + } + + private async decryptResponsePayload( + payload: EncString, + key: string + ): Promise { + const sharedKey = await this.getSharedKeyForKey(key); + const decrypted = await this.encryptService.decryptToUtf8(payload, sharedKey); + + return JSON.parse(decrypted); + } + + private async getSharedKeyForKey(key: string): Promise { + const dataBuffer = Utils.fromB64ToArray(key).buffer; + const privKey = Utils.fromB64ToArray(config.testRsaPrivateKey).buffer; + + return new SymmetricCryptoKey( + await this.nodeCryptoFunctionService.rsaDecrypt(dataBuffer, privKey, "sha1") + ); + } +} diff --git a/apps/desktop/native-messaging-test-runner/src/race.ts b/apps/desktop/native-messaging-test-runner/src/race.ts new file mode 100644 index 00000000000..7aba3aa41f9 --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/race.ts @@ -0,0 +1,25 @@ +export const race = ({ + promise, + timeout, + error, +}: { + promise: Promise; + timeout: number; + error?: Error; +}) => { + let timer = null; + + // Similar to Promise.all, but instead of waiting for all, it resolves once one promise finishes. + // Using this so we can reject if the timeout threshold is hit + return Promise.race([ + new Promise((_, reject) => { + timer = setTimeout(reject, timeout, error); + return timer; + }), + + promise.then((value) => { + clearTimeout(timer); + return value; + }), + ]); +}; diff --git a/apps/desktop/native-messaging-test-runner/src/variables.ts b/apps/desktop/native-messaging-test-runner/src/variables.ts new file mode 100644 index 00000000000..973da2c224b --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/src/variables.ts @@ -0,0 +1,27 @@ +export const applicationName = "Native Messaging Test Runner"; +export const encryptionAlogrithm = "sha1"; +export const testRsaPublicKey = + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + + "RQ1H7iNHVZz3K8Db9GCSKPeC8MbW6gVCzb15esCe1gGzg6wkMuWYDFYPoh/oBqcIqrGah7firqB1nDedzEjw32heP2DAffVN" + + "084iTDjiWrJNUxBJ2pDD5Z9dT3MzQ2s09ew1yMWK2z37rT3YerC7OgEDmo3WYo3xL3qYJznu3EO2nmrYjiRa40wKSjxsTlUc" + + "xDF+F0uMW8oR9EMUHgepdepfAtLsSAQIDAQAB"; +export const testRsaPrivateKey = + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXRVrCX+2hfOQS8Hz" + + "YUS2oc/jGVTZpv+/Ryuoh9d8ihYX9dd0cYh2tl6KWdFc88lPUH11Oxqy20Rk2e5r/RF6T9yM0Me3NPnaKt+hlhLtfoc0h86L" + + "nhD56A9FDUfuI0dVnPcrwNv0YJIo94LwxtbqBULNvXl6wJ7WAbODrCQy5ZgMVg+iH+gGpwiqsZqHt+KuoHWcN53MSPDfaF4/" + + "YMB99U3TziJMOOJask1TEEnakMPln11PczNDazT17DXIxYrbPfutPdh6sLs6AQOajdZijfEvepgnOe7cQ7aeatiOJFrjTApK" + + "PGxOVRzEMX4XS4xbyhH0QxQeB6l16l8C0uxIBAgMBAAECggEASaWfeVDA3cVzOPFSpvJm20OTE+R6uGOU+7vh36TX/POq92q" + + "Buwbd0h0oMD32FxsXywd2IxtBDUSiFM9699qufTVuM0Q3tZw6lHDTOVG08+tPdr8qSbMtw7PGFxN79fHLBxejjO4IrM9lapj" + + "WpxEF+11x7r+wM+0xRZQ8sNFYG46aPfIaty4BGbL0I2DQ2y8I57iBCAy69eht59NLMm27fRWGJIWCuBIjlpfzET1j2HLXUIh" + + "5bTBNzqaN039WH49HczGE3mQKVEJZc/efk3HaVd0a1Sjzyn0QY+N1jtZN3jTRbuDWA1AknkX1LX/0tUhuS3/7C3ejHxjw4Dk" + + "1ZLo5/QKBgQDIWvqFn0+IKRSu6Ua2hDsufIHHUNLelbfLUMmFthxabcUn4zlvIscJO00Tq/ezopSRRvbGiqnxjv/mYxucvOU" + + "BeZtlus0Q9RTACBtw9TGoNTmQbEunJ2FOSlqbQxkBBAjgGEppRPt30iGj/VjAhCATq2MYOa/X4dVR51BqQAFIEwKBgQDBSIf" + + "TFKC/hDk6FKZlgwvupWYJyU9RkyfstPErZFmzoKhPkQ3YORo2oeAYmVUbS9I2iIYpYpYQJHX8jMuCbCz4ONxTCuSIXYQYUcU" + + "q4PglCKp31xBAE6TN8SvhfME9/MvuDssnQinAHuF0GDAhF646T3LLS1not6Vszv7brwSoGwKBgQC88v/8cGfi80ssQZeMnVv" + + "q1UTXIeQcQnoY5lGHJl3K8mbS3TnXE6c9j417Fdz+rj8KWzBzwWXQB5pSPflWcdZO886Xu/mVGmy9RWgLuVFhXwCwsVEPjNX" + + "5ramRb0/vY0yzenUCninBsIxFSbIfrPtLUYCc4hpxr+sr2Mg/y6jpvQKBgBezMRRs3xkcuXepuI2R+BCXL1/b02IJTUf1F+1" + + "eLLGd7YV0H+J3fgNc7gGWK51hOrF9JBZHBGeOUPlaukmPwiPdtQZpu4QNE3l37VlIpKTF30E6mb+BqR+nht3rUjarnMXgAoE" + + "Z18y6/KIjpSMpqC92Nnk/EBM9EYe6Cf4eA9ApAoGAeqEUg46UTlJySkBKURGpIs3v1kkf5I0X8DnOhwb+HPxNaiEdmO7ckm8" + + "+tPVgppLcG0+tMdLjigFQiDUQk2y3WjyxP5ZvXu7U96jaJRI8PFMoE06WeVYcdIzrID2HvqH+w0UQJFrLJ/0Mn4stFAEzXKZ" + + "BokBGnjFnTnKcs7nv/O8="; diff --git a/apps/desktop/native-messaging-test-runner/tsconfig.json b/apps/desktop/native-messaging-test-runner/tsconfig.json new file mode 100644 index 00000000000..a34554a264f --- /dev/null +++ b/apps/desktop/native-messaging-test-runner/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "baseUrl": "./", + "outDir": "dist", + "target": "es6", + "module": "CommonJS", + "moduleResolution": "node", + "sourceMap": false, + "declaration": false, + "paths": { + "@src/*": ["src/*"], + "@bitwarden/node/*": ["../../../libs/node/src/*"], + "@bitwarden/common/*": ["../../../libs/common/src/*"] + } + }, + "exclude": ["node_modules"] +} diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 420ee20a39e..a1ecc0732f9 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -309,6 +309,23 @@ {{ "enableBrowserIntegrationDesc" | i18n }} +
+
+ +
+ {{ + "enableDuckDuckGoBrowserIntegrationDesc" | i18n + }} +