diff --git a/.gitignore b/.gitignore index 25851242fda3ea71887611907a5fa39836c782bd..7cd2be0c5bfd5752ee08ea7b9c8d31284698cd69 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ translationtool.phar .php-cs-fixer.cache .phpunit.result.cache junit.xml +css/ +.codex diff --git a/README.md b/README.md index ae3cbfe279036d444d7467776679cad9b24c90ae..2ee0f4ba209124378ef9913b474b04371a8a4d5a 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ ## User Account creation -- This plugin creates an endpoint `/apps/ecloud-accounts/api/set_account_data` that is to be used to set user's email, quota,recovery email and create the user's folder if necessary +- Form for user account creation accessible from `/apps/ecloud-accounts/accounts/{lang}/signup` +- For development info on account creation, see [account creation docs](docs/account-creation.md) ### Captcha Configuration for user account creation @@ -26,6 +27,13 @@ - `ecloud-accounts.hcaptcha_site_key` - `ecloud-accounts.hcaptcha_secret` +### Passwords E2EE setup at signup configuration (disabled by default) + +- If you want to enforce passwords E2EE setup at signup using the account password set by user: + `occ config:app:set ecloud-accounts setupPasswordsE2eeAtSignup --value true --type=boolean` +- To disable: + `occ config:app:set ecloud-accounts setupPasswordsE2eeAtSignup --value false --type=boolean` + ## Drop account - The drop account functionality plugin works in conjunction with the drop_account plugin : https://apps.nextcloud.com/apps/drop_account @@ -151,4 +159,4 @@ Needs configuration variables to be set: ``` oidc_vault_account_delete_url=vault-full-url oidc_vault_account_delete_token=vault-token -``` \ No newline at end of file +``` diff --git a/docs/account-creation.md b/docs/account-creation.md new file mode 100644 index 0000000000000000000000000000000000000000..b950e0539acb305c9b65ed9f0abea57912c177eb --- /dev/null +++ b/docs/account-creation.md @@ -0,0 +1,40 @@ +### Actions done when the create method is triggered + +- **Note: Steps that fail with an error are in bold; other steps fail silently in the background** + +### When passwords E2EE setup at signup is disabled + +```mermaid +flowchart TD + K{Browser} --> L{Server} + L --> A[1. Validation] + A --> B[2. Creation of LDAP record] + B --> C[3. Add username to permanent common data store] + C --> D[4. Set user data locally at nextcloud] + D --> E[5. Create HME and default @murena.io aliases] + E --> F[6. Set user ToS as accepted] + F --> G[7. Set user language] + G --> H[8. Set Newsletter signup choice of user] + H --> I[9. Set recovery email of user and trigger verification email] + I --> J[10. Trigger welcome email] +``` + +### When passwords E2EE setup at signup is enabled + +```mermaid +flowchart TD + K{Browser} --> L{Server} + L --> A[1. Validation] + A --> B[2. Creation of LDAP record] + B --> C[3. Add username to permanent common data store] + C --> D[4. Set user data locally at nextcloud] + D --> E[5. Create HME and default @murena.io aliases] + E --> F[6. Set user ToS as accepted] + F --> G[7. Set user language] + G --> H[8. Set Newsletter signup choice of user] + H --> I[9. Set recovery email of user and trigger verification email] + I --> J[10. Trigger welcome email] + J --> |Success| K + K --> N[12. Compute and setup user encryption challenge at server via API call] + N --> O[13. Setup user keychain at server via API call] +``` diff --git a/l10n/de.js b/l10n/de.js index 7b58fc509174f8d5d2ad928c86074f57bba847c4..5a5a6a83bc48f762b3dccbb64018836a806f0f6d 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -84,6 +84,7 @@ OC.L10N.register( "Username is too large.": "Der Benutzername ist zu groß.", "Display name is too large.": "Der Anzeigename ist zu groß.", "Password has invalid characters.": "Das Passwort enthält ungültige Zeichen.", - "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten." + "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Ihr Browser unterstützt die erforderliche Verschlüsselung nicht. Bitte aktualisieren Sie Ihren Browser, um fortzufahren." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de.json b/l10n/de.json index 30bd2d7381813dd2e120cd4736dbdf4156cdbdd6..06d7bc6d6a7c1f8fc378a78c37f133557acfa7fc 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -82,7 +82,8 @@ "Username is too large.": "Der Benutzername ist zu groß.", "Display name is too large.": "Der Anzeigename ist zu groß.", "Password has invalid characters.": "Das Passwort enthält ungültige Zeichen.", - "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten." + "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Ihr Browser unterstützt die erforderliche Verschlüsselung nicht. Bitte aktualisieren Sie Ihren Browser, um fortzufahren." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/l10n/de_DE.js b/l10n/de_DE.js index 815e0347ae0672e2be39addb517d117419c84263..a45189345291262b3936e8d52e8b7076010a4c31 100644 --- a/l10n/de_DE.js +++ b/l10n/de_DE.js @@ -84,6 +84,7 @@ OC.L10N.register( "Username is too large.": "Der Benutzername ist zu groß.", "Display name is too large.": "Der Anzeigename ist zu groß.", "Password has invalid characters.": "Das Passwort enthält ungültige Zeichen.", - "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten." + "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Ihr Browser unterstützt die erforderliche Verschlüsselung nicht. Bitte aktualisieren Sie Ihren Browser, um fortzufahren." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/de_DE.json b/l10n/de_DE.json index 7a3967f970b41afba66acae202f4085aff41cbb4..ae9c75fe7d9a1f5ed6ea646398cd7646725c6f3c 100644 --- a/l10n/de_DE.json +++ b/l10n/de_DE.json @@ -82,7 +82,8 @@ "Username is too large.": "Der Benutzername ist zu groß.", "Display name is too large.": "Der Anzeigename ist zu groß.", "Password has invalid characters.": "Das Passwort enthält ungültige Zeichen.", - "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten." + "I want to stay informed about Murena for business offers.": "Ich möchte Informationen über Murenas Angebote erhalten.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Ihr Browser unterstützt die erforderliche Verschlüsselung nicht. Bitte aktualisieren Sie Ihren Browser, um fortzufahren." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/l10n/en.js b/l10n/en.js index e8ea893704d93627ebaccfef2ba7756c33dfe508..9209b1ff4550b3636fe50b04881ec6f861f28e9a 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -87,6 +87,7 @@ OC.L10N.register( "Username is too large.": "Username is too large.", "Display name is too large.": "Display name is too large.", "Password has invalid characters.": "Password has invalid characters.", - "I want to stay informed about Murena for business offers.": "I want to stay informed about Murena for business offers." + "I want to stay informed about Murena for business offers.": "I want to stay informed about Murena for business offers.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Your browser does not support the required encryption. Please use an up-to-date browser to continue." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/en.json b/l10n/en.json index d0c84c9c29fbe2b74bfed49fdba64f2af532865f..d6e8e1ad6897ce7e07fd538c5355e5c0a3a0f007 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -84,7 +84,8 @@ "Username is too large.": "Username is too large.", "Display name is too large.": "Display name is too large.", "Password has invalid characters.": "La contraseña tiene caracteres no válidos.", - "I want to stay informed about Murena for business offers.": "I want to stay informed about Murena for business offers." + "I want to stay informed about Murena for business offers.": "I want to stay informed about Murena for business offers.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Your browser does not support the required encryption. Please use an up-to-date browser to continue." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/l10n/es.js b/l10n/es.js index 2a134171d2550fb27efcb63659b88798040f9ed6..332318689ce74e0901c9ac916b679c71d111adc5 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -86,6 +86,7 @@ OC.L10N.register( "Username is too large.": "El nombre de usuario es demasiado grande.", "Display name is too large.": "El nombre para mostrar es demasiado grande.", "Password has invalid characters.": "La contraseña tiene caracteres no válidos.", - "I want to stay informed about Murena for business offers.": "Deseo permanecer informado sobre las ofertas de Murena para empresas." + "I want to stay informed about Murena for business offers.": "Deseo permanecer informado sobre las ofertas de Murena para empresas.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Tu navegador no admite el cifrado requerido. Utiliza un navegador actualizado para continuar." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/es.json b/l10n/es.json index de0fde984696c429e8eabac333ac65ec22a62d7a..88c4ba50a1d4c57c25e617a9ef94eb91cba01667 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -85,7 +85,8 @@ "Username is too large.": "El nombre de usuario es demasiado grande.", "Display name is too large.": "El nombre para mostrar es demasiado grande.", "Password has invalid characters.": "Password has invalid characters.", - "I want to stay informed about Murena for business offers.": "Deseo permanecer informado sobre las ofertas de Murena para empresas." + "I want to stay informed about Murena for business offers.": "Deseo permanecer informado sobre las ofertas de Murena para empresas.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Tu navegador no admite el cifrado requerido. Utiliza un navegador actualizado para continuar." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/l10n/fr.js b/l10n/fr.js index 6e94f030a4071a45e0581badace999bbd1fd3a70..25481a169e2b1eaac3855a57a6c0eb870a52dcd1 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -85,6 +85,7 @@ OC.L10N.register( "Username is too large.": "Le nom d'utilisateur est trop grand.", "Display name is too large.": "Le nom affiché est trop grand.", "Password has invalid characters.": "Le mot de passe contient des caractères non valides.", - "I want to stay informed about Murena for business offers.": "Je souhaite rester informé·e des offres Murena pour les entreprises." + "I want to stay informed about Murena for business offers.": "Je souhaite rester informé·e des offres Murena pour les entreprises.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Votre navigateur ne prend pas en charge le chiffrement requis. Veuillez utiliser un navigateur à jour pour continuer." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/fr.json b/l10n/fr.json index 288c8749b26431e49b0d58cebe1c5c6d59c8d2e1..2c80d3a507ea3b3866bdd583e28d2dc281fc7991 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -84,7 +84,8 @@ "Username is too large.": "Le nom d'utilisateur est trop grand.", "Display name is too large.": "Le nom affiché est trop grand.", "Password has invalid characters.": "Le mot de passe contient des caractères non valides.", - "I want to stay informed about Murena for business offers.": "Je souhaite rester informé·e des offres Murena pour les entreprises." + "I want to stay informed about Murena for business offers.": "Je souhaite rester informé·e des offres Murena pour les entreprises.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Votre navigateur ne prend pas en charge le chiffrement requis. Veuillez utiliser un navigateur à jour pour continuer." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/l10n/it.js b/l10n/it.js index e804cb1d16baf3dba1eb8c44bda143ebffcb8f61..a18bfeab08728ae99034532bb0b7f8d5f5f42457 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -85,6 +85,7 @@ OC.L10N.register( "Username is too large.": "Il nome utente è troppo grande.", "Display name is too large.": "Il nome del display è troppo grande.", "Password has invalid characters.": "La password contiene caratteri non validi.", - "I want to stay informed about Murena for business offers.": "Desidero ricevere informazioni sulle offerte commerciali di Murena." + "I want to stay informed about Murena for business offers.": "Desidero ricevere informazioni sulle offerte commerciali di Murena.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Il tuo browser non supporta la crittografia richiesta. Utilizza un browser aggiornato per continuare." }, "nplurals=2; plural=(n != 1);"); diff --git a/l10n/it.json b/l10n/it.json index 1bacf80b539de51d97e40a54ca2fb48929a7798e..3989a8d8fe458036c8803e0a0e35f25149f13147 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -81,7 +81,8 @@ "Display name is too large.": "Il nome del display è troppo grande.", "Password has invalid characters.": "La password contiene caratteri non validi.", "I have read and accept the Terms of Service.": "Ho letto e accetto i Termini di servizio.", - "I want to stay informed about Murena for business offers.": "Desidero ricevere informazioni sulle offerte commerciali di Murena." + "I want to stay informed about Murena for business offers.": "Desidero ricevere informazioni sulle offerte commerciali di Murena.", + "Your browser does not support the required encryption. Please use an up-to-date browser to continue.": "Il tuo browser non supporta la crittografia richiesta. Utilizza un browser aggiornato per continuare." }, "pluralForm": "nplurals=2; plural=(n != 1);" } diff --git a/lib/Controller/AccountController.php b/lib/Controller/AccountController.php index 54d316accefa021974cf781b6bd63f5a05c68f69..379d2e0fa56e1fa6e37f76fab5b268d17b3a01a3 100644 --- a/lib/Controller/AccountController.php +++ b/lib/Controller/AccountController.php @@ -14,6 +14,7 @@ use OCA\EcloudAccounts\Service\CaptchaService; use OCA\EcloudAccounts\Service\HCaptchaService; use OCA\EcloudAccounts\Service\NewsLetterService; use OCA\EcloudAccounts\Service\UserService; +use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; @@ -52,7 +53,8 @@ class AccountController extends Controller { private const HCAPTCHA_PROVIDER = 'hcaptcha'; private const HCAPTCHA_DOMAINS = ['https://hcaptcha.com', 'https://*.hcaptcha.com']; private const BLACKLISTED_USERNAMES_FILE_NAME = 'blacklisted_usernames'; - + private const PASSWORDS_APP_ID = 'passwords'; + private LoggerInterface $logger; public function __construct( $AppName, @@ -66,6 +68,7 @@ class AccountController extends Controller { IURLGenerator $urlGenerator, ISession $session, IConfig $config, + private IAppManager $appManager, LoggerInterface $logger, IInitialState $initialState, IAppData $appData, @@ -113,18 +116,27 @@ class AccountController extends Controller { $captchaProvider = $this->getCaptchaProvider(); $this->initialState->provideInitialState('captchaProvider', $captchaProvider); + $csp = $response->getContentSecurityPolicy(); + $setupPasswordsE2ee = false; + + if ($this->shouldSetupPasswordsE2eeAtSignup()) { + $setupPasswordsE2ee = true; + $csp->allowEvalWasm(); + } + $this->initialState->provideInitialState('shouldSetupPasswordsE2ee', $setupPasswordsE2ee); + if ($captchaProvider === self::HCAPTCHA_PROVIDER) { - $csp = $response->getContentSecurityPolicy(); foreach (self::HCAPTCHA_DOMAINS as $domain) { $csp->addAllowedScriptDomain($domain); $csp->addAllowedFrameDomain($domain); $csp->addAllowedStyleDomain($domain); $csp->addAllowedConnectDomain($domain); } - $response->setContentSecurityPolicy($csp); $hcaptchaSiteKey = $this->config->getSystemValue(Application::APP_ID . '.hcaptcha_site_key'); $this->initialState->provideInitialState('hCaptchaSiteKey', $hcaptchaSiteKey); } + $response->setContentSecurityPolicy($csp); + return $response; } @@ -171,7 +183,7 @@ class AccountController extends Controller { $inputData = [ 'username' => ['value' => $username, 'maxLength' => 30], 'display name' => ['value' => $displayname, 'maxLength' => 30], - 'password' => ['value' => $password, 'maxLength' => 1024], + 'password' => ['value' => $password, 'maxLength' => 32], ]; foreach ($inputData as $inputName => $inputInfo) { @@ -218,6 +230,11 @@ class AccountController extends Controller { $this->session->remove(self::SESSION_VERIFIED_DISPLAYNAME); $this->session->remove(self::CAPTCHA_VERIFIED_CHECK); + // Session needed for client to setup E2EE at passwords app via API calls + if ($this->shouldSetupPasswordsE2eeAtSignup()) { + $user = $this->userService->getUser($username); + $this->userSession->setUser($user); + } $response->setStatus(200); $response->setData(['success' => true]); @@ -408,4 +425,8 @@ class AccountController extends Controller { return $captchaProvider; } + private function shouldSetupPasswordsE2eeAtSignup() : bool { + return $this->appManager->isEnabledForUser(self::PASSWORDS_APP_ID) && $this->config->getAppValue(Application::APP_ID, 'setupPasswordsE2eeAtSignup', false); + } + } diff --git a/package-lock.json b/package-lock.json index 3cd4fc6c4da5900612973cd17651f0241f54b8df..a383b43696a37cf3c0ffd20ad1a4006042e9b163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nextcloud/l10n": "^3.1.0", "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.16.0", + "passwords-client": "1.0.0-alpha.5731", "vue": "^2.7.0", "vue-password-strength-meter": "^1.7.2" }, @@ -9325,6 +9326,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libsodium": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.10.tgz", + "integrity": "sha512-eY+z7hDrDKxkAK+QKZVNv92A5KYkxfvIshtBJkmg5TSiCnYqZP3i9OO9whE79Pwgm4jGaoHgkM4ao/b9Cyu4zQ==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", + "integrity": "sha512-pO3F1Q9NPLB/MWIhehim42b/Fwb30JNScCNh8TcQ/kIc+qGLQch8ag8wb0keK3EP5kbGakk1H8Wwo7v+36rNQg==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.7.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11130,6 +11146,26 @@ "node": ">= 0.8" } }, + "node_modules/passwords-client": { + "version": "1.0.0-alpha.5731", + "resolved": "https://registry.npmjs.org/passwords-client/-/passwords-client-1.0.0-alpha.5731.tgz", + "integrity": "sha512-Pl36mcHhUlbN9IjJAkPmcunXPvC5Rz/hVZZgUDKM+GOUIzVSsH8ltPliUJCN+2tugc0x/aINmO987CXNptWcYw==", + "license": "ISC", + "dependencies": { + "eventemitter3": "^3.1.2", + "libsodium": "0.7.10", + "libsodium-wrappers": "0.7.10", + "process": "^0.11.10", + "url-parse": "^1.5.3", + "uuid": "^8.3.2" + } + }, + "node_modules/passwords-client/node_modules/eventemitter3": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -11566,8 +11602,6 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "peer": true, "engines": { "node": ">= 0.6.0" } @@ -14418,8 +14452,6 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "peer": true, "bin": { "uuid": "dist/bin/uuid" } diff --git a/package.json b/package.json index 58337c9e399aa671bbba6a69aa503dbda148c2ec..0337d1710e0b80cbb44af2e4b852a56ef19f6584 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "@nextcloud/router": "^3.0.1", "@nextcloud/vue": "^8.16.0", "vue": "^2.7.0", - "vue-password-strength-meter": "^1.7.2" + "vue-password-strength-meter": "^1.7.2", + "passwords-client": "1.0.0-alpha.5731" }, "browserslist": [ "extends @nextcloud/browserslist-config" diff --git a/src/Signup.vue b/src/Signup.vue index 9f0d163c0547d2ab2c2c6c5cebeac2e8cbb9b705..e3615469f7c09694213e1ef7948b5c284ed9dc57 100644 --- a/src/Signup.vue +++ b/src/Signup.vue @@ -2,7 +2,10 @@
- + @@ -30,6 +33,8 @@ import HCaptchaForm from './signup/HCaptchaForm.vue' import CaptchaForm from './signup/CaptchaForm.vue' import RecoveryEmailForm from './signup/RecoveryEmailForm.vue' import SuccessSection from './signup/SuccessSection.vue' +import ClientService from './passwords/ClientService.js' +import LegacyPasswordsApi from './passwords/LegacyPasswordsApi.js' const APPLICATION_NAME = 'ecloud-accounts' @@ -62,6 +67,8 @@ export default { showCaptchaForm: false, showRecoveryEmailForm: false, showSuccessSection: false, + shouldSetupPasswordsE2ee: loadState(APPLICATION_NAME, 'shouldSetupPasswordsE2ee'), + cryptoSupported: true, language: loadState(APPLICATION_NAME, 'lang'), processingCreation: false, } @@ -73,8 +80,32 @@ export default { // Set formData.email directly to recoveryEmail this.formData.email = recoveryEmail || '' + this.checkCryptoSupport() }, methods: { + async checkCryptoSupport() { + if (!this.shouldSetupPasswordsE2ee) { + return + } + + try { + const hasWebCrypto = typeof window.crypto !== 'undefined' + && typeof window.crypto.subtle === 'object' + const hasTextEncoder = typeof window.TextEncoder !== 'undefined' + const hasWebAssembly = typeof window.WebAssembly !== 'undefined' + && typeof window.WebAssembly.instantiate === 'function' + + if (!hasWebCrypto || !hasTextEncoder || !hasWebAssembly) { + throw new Error('Required cryptography APIs are unavailable') + } + + this.cryptoSupported = true + } catch (error) { + this.cryptoSupported = false + const cryptoSupportError = t(this.appName, 'Your browser does not support the cryptography required to create an account. Please use a modern browser.') + showError(cryptoSupportError, { timeout: -1 }) + } + }, submitRegistrationForm(data) { if (data.isFormValid) { this.showRegistrationForm = false @@ -89,7 +120,7 @@ export default { this.showRecoveryEmailForm = true } }, - submitRecoveryEmailForm(data) { + async submitRecoveryEmailForm(data) { if (data.isFormValid) { const data = { password: this.formData.password, @@ -97,9 +128,9 @@ export default { language: this.formData.selectedLanguage, newsletterEos: this.formData.newsletterEos, newsletterProduct: this.formData.newsletterProduct, - newsletterB2B: this.formData.newsletterB2B + newsletterB2B: this.formData.newsletterB2B, } - this.submitForm(data) + await this.submitForm(data) } }, async submitForm(data) { @@ -107,6 +138,9 @@ export default { const url = generateUrl(`/apps/${this.appName}/accounts/create`) this.processingCreation = true await Axios.post(url, data) + if (this.shouldSetupPasswordsE2ee) { + await this.setupPasswordsE2ee() + } // If the execution reaches here, the response status is in the 2xx range this.showRegistrationForm = false @@ -125,6 +159,24 @@ export default { this.showMessage(errorMessage, 'error') } }, + async setupPasswordsE2ee() { + const token = this.formData.password + const user = this.formData.username + let baseUrl = new URL(generateUrl('/', [], {}), location.origin).href + if (baseUrl.indexOf('index.php') !== -1) { + baseUrl = baseUrl.substr(0, baseUrl.indexOf('index.php')) + } + + ClientService.initialize(baseUrl, user, token, { cseMode: 'CSEv1r1' }) + const api = new LegacyPasswordsApi() + api.initialize(ClientService.getClient(), { cseMode: 'CSEv1r1' }) + try { + await api.setAccountChallenge(token) + } catch (error) { + console.error('[passwords] setAccountChallenge failed', error) + throw error + } + }, showMessage(message, type) { type === 'success' ? showSuccess(message) : showError(message) }, diff --git a/src/passwords/ApiRequest.js b/src/passwords/ApiRequest.js new file mode 100644 index 0000000000000000000000000000000000000000..d3439cf2a4da8052299bf2159d016a923e5256c0 --- /dev/null +++ b/src/passwords/ApiRequest.js @@ -0,0 +1,22 @@ +import { ApiRequest as OriginalApiRequest } from 'passwords-client/http' + +export default class ApiRequest extends OriginalApiRequest { + + /** + * @param {string} url + * @param {object} options + * @return {Promise} + * @private + */ + async _executeRequest(url, options) { + try { + const request = new Request(url, options) + this._api.emit('request.before', request) + return await fetch(request, options) + } catch (e) { + this._api.emit('request.error', e) + throw e + } + } + +} diff --git a/src/passwords/ClientService.js b/src/passwords/ClientService.js new file mode 100644 index 0000000000000000000000000000000000000000..4ec4b8ff699a12707cadb39bb182136d51577eca --- /dev/null +++ b/src/passwords/ClientService.js @@ -0,0 +1,40 @@ +/* eslint-disable new-parens */ +/* eslint-disable padded-blocks */ +import EnhancedClassLoader from 'passwords-client/enhanced-class-loader' +import PasswordsClient from 'passwords-client' +import LegacyPasswordsApi from './LegacyPasswordsApi.js' +import ApiRequest from './ApiRequest.js' +import PWDv1Challenge from './PWDv1Challenge.js' + +export default new class ClientService { + constructor() { + this._client = null + } + + initialize(baseUrl, user, token, legacyConfig = {}) { + const server = { baseUrl, user, token } + const config = { baseUrl, user, token } + + const classes = { + legacy: () => { + const client = new LegacyPasswordsApi() + client.initialize(this.getClient(), legacyConfig) + return client + }, + // Need to over-ride the challenge class to set accepted password length to 8-32 characters + 'challenge.pwdv1': PWDv1Challenge, + 'network.request': ApiRequest, + } + + const classLoader = new EnhancedClassLoader(classes) + this._client = new PasswordsClient(server, config, classLoader) + } + + getClient() { + return this._client + } + + getLegacyClient() { + return this.getClient().getInstance('legacy') + } +} diff --git a/src/passwords/LegacyPasswordsApi.js b/src/passwords/LegacyPasswordsApi.js new file mode 100644 index 0000000000000000000000000000000000000000..aed85dbf9d0ed15e88ec1f9f404e2fb47e7591c9 --- /dev/null +++ b/src/passwords/LegacyPasswordsApi.js @@ -0,0 +1,17 @@ +import { EnhancedApi } from 'passwords-client/legacy' + +export default class LegacyPasswordsApi extends EnhancedApi { + + async _executeRequest(url, options) { + try { + const request = new Request(url, options) + this._config.events.emit('api.request.before', request) + return await fetch(request, options) + } catch (e) { + if (e.status === 401 && this._enabled) this._enabled = false + this._config.events.emit('api.request.error', e) + throw e + } + } + +} diff --git a/src/passwords/PWDv1Challenge.js b/src/passwords/PWDv1Challenge.js new file mode 100644 index 0000000000000000000000000000000000000000..939fa832f57190154ffd8a2a5314cdbf8d748a10 --- /dev/null +++ b/src/passwords/PWDv1Challenge.js @@ -0,0 +1,72 @@ +import sodium from 'libsodium-wrappers' +import DefaultClassLoader from 'passwords-client/default-class-loader' + +const classLoader = new DefaultClassLoader() +const BasePWDv1Challenge = classLoader._classes['challenge.pwdv1'] + +export default class PWDv1Challenge extends BasePWDv1Challenge { + constructor(data = {}) { + super(data) + } + + _validatePasswordLength() { + if (this._password.length < 8) throw new Error('Password is too short') + if (this._password.length > 128) throw new Error('Password is too long') + } + + solve() { + this._validatePasswordLength() + const salts = this._salts + + const passwordSalt = sodium.from_hex(salts[0]) + const genericHashKey = sodium.from_hex(salts[1]) + const passwordHashSalt = sodium.from_hex(salts[2]) + const genericHash = sodium.crypto_generichash( + sodium.crypto_generichash_BYTES_MAX, + new Uint8Array([...sodium.from_string(this._password), ...passwordSalt]), + genericHashKey + ) + + const passwordHash = sodium.crypto_pwhash( + sodium.crypto_box_SEEDBYTES, + genericHash, + passwordHashSalt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ) + + return sodium.to_hex(passwordHash) + } + + create() { + this._validatePasswordLength() + + const passwordSalt = sodium.randombytes_buf(256) + const genericHashKey = sodium.randombytes_buf(sodium.crypto_generichash_KEYBYTES_MAX) + const genericHash = sodium.crypto_generichash( + sodium.crypto_generichash_BYTES_MAX, + new Uint8Array([...sodium.from_string(this._password), ...passwordSalt]), + genericHashKey + ) + + const passwordHashSalt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES) + const passwordHash = sodium.crypto_pwhash( + sodium.crypto_box_SEEDBYTES, + genericHash, + passwordHashSalt, + sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, + sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE, + sodium.crypto_pwhash_ALG_DEFAULT + ) + + return { + salts: [ + sodium.to_hex(passwordSalt), + sodium.to_hex(genericHashKey), + sodium.to_hex(passwordHashSalt), + ], + secret: sodium.to_hex(passwordHash), + } + } +} diff --git a/src/settings-user-security.js b/src/settings-user-security.js index 809e9cdac4227a5ba864a182119a7f19ac77e398..3160d192fd8f70c7b3b2a7f6d9556cebea5e8a1f 100644 --- a/src/settings-user-security.js +++ b/src/settings-user-security.js @@ -1,6 +1,9 @@ (function() { const OriginalXhr = window.XMLHttpRequest + /** + * + */ function PatchedXhr() { const xhr = new OriginalXhr() diff --git a/src/signup/RegistrationForm.vue b/src/signup/RegistrationForm.vue index 15fad12dc264593fc503bcde6257abf7a8035907..51db07a1ff7b94ea8ae7a46055d5ae066ec1bde3 100644 --- a/src/signup/RegistrationForm.vue +++ b/src/signup/RegistrationForm.vue @@ -1,7 +1,9 @@