diff --git a/includes/core/load.js.php b/includes/core/load.js.php index a6b8d4b32..e19c49bbc 100755 --- a/includes/core/load.js.php +++ b/includes/core/load.js.php @@ -2082,4 +2082,69 @@ function hashUserId(userId) { const hash = CryptoJS.SHA256(userId); return hash.toString(CryptoJS.enc.Hex).substring(0, 16); } + + /** + * Get item password to show or copy it in clipboard. + * + * @param {string} action - Log action (ex: at_password_shown). + * @param {string} id_type - 'item_key' or 'item_id'. + * @param {number|string} id_value - The item key or id. + * + * @returns {string} - The item cleartext password if user has access. + */ + function getItemPassword(action, id_type, id_value) { + let item_password = ''; + + // Get password from server + $.ajax({ + type: "POST", + async: false, + url: 'sources/items.queries.php', + data: 'type=get_item_password&action=' + action + '&' + id_type + + '=' + id_value + '&key=get('key'); ?>', + dataType: "", + success: function(data) { + //decrypt data + try { + data = prepareExchangedData(data, "decode", "get('key'); ?>"); + } catch (e) { + // error + toastr.remove(); + toastr.warning( + 'get('no_item_to_display'); ?>' + ); + return false; + } + + // No access + if (data.password_error !== '') { + toastr.remove(); + toastr.error( + data.password_error, + 'get('caution'); ?>', { + timeOut: 5000, + progressBar: true + } + ); + return false; + } + + const password = simplePurifier(atob(data.password), false, false, false, false).utf8Decode(); + if (password === '') { + toastr.info( + 'get('password_is_empty'); ?>', + '', { + timeOut: 2000, + positionClass: 'toast-bottom-right', + progressBar: true + } + ); + } + + item_password = password; + } + }); + + return item_password; + } diff --git a/includes/core/login.js.php b/includes/core/login.js.php index d93ee8fd9..3fd78b724 100755 --- a/includes/core/login.js.php +++ b/includes/core/login.js.php @@ -627,11 +627,7 @@ function launchIdentify(isDuo, redirect, psk, oauth2 = false) { //TODO : je pense que cela pourrait etre modifié pour ne pas faire de requete ajax ; on dispose des infos via `get_teampass_settings` $.post( 'sources/identify.php', { - type: 'get2FAMethods', - login: $('#login').val(), - xhrFields: { - withCredentials: true - } + type: 'get2FAMethods' }, function(data) { data = JSON.parse(data); diff --git a/includes/language/bulgarian.php b/includes/language/bulgarian.php index e02fc1121..9e4f103a0 100755 --- a/includes/language/bulgarian.php +++ b/includes/language/bulgarian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/catalan.php b/includes/language/catalan.php index 3df3a347f..650c8c0a9 100755 --- a/includes/language/catalan.php +++ b/includes/language/catalan.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/chinese.php b/includes/language/chinese.php index 2743f9f81..6b4bbf6f2 100755 --- a/includes/language/chinese.php +++ b/includes/language/chinese.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/czech.php b/includes/language/czech.php index 7846aabb2..46505d7a3 100755 --- a/includes/language/czech.php +++ b/includes/language/czech.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/dutch.php b/includes/language/dutch.php index cae5a7d48..444df9802 100755 --- a/includes/language/dutch.php +++ b/includes/language/dutch.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/english.php b/includes/language/english.php index 999b3ceca..57b99d8e5 100755 --- a/includes/language/english.php +++ b/includes/language/english.php @@ -39,7 +39,6 @@ 'highlight_selected_tip' => 'When enabled, the selected item will be highlighted in the list.', 'highlight_favorites' => 'Highlight favorites', 'highlight_favorites_tip' => 'When enabled, the favorite items will be highlighted in the list.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'oauth2_need_user_old_password' => 'For the first SSO connection, please provide your previous password', @@ -1183,6 +1182,10 @@ 'syslog_port' => 'Syslog port (default 514)', 'error_bad_credentials' => 'Login credentials do not correspond!', 'bruteforce_wait' => 'Too many failed attempts, your account is blocked until: ', + 'bruteforce_unlock_at' => 'Account unlocked at (anti bruteforce): ', + 'bruteforce_reset_account' => 'Reset anti bruteforce of user', + 'bruteforce_reset_mail_subject' => 'TEAMPASS - Your account is disabled', + 'bruteforce_reset_mail_body' => 'Hello #name#,

Your teampass account has been locked due to a large number of authentication failures.

You can unblock it by clicking on this link #reset_url#

Automatic unlock: #unlock_at#', 'settings_ldap_usergroup' => 'LDAP group to search', 'settings_ldap_usergroup_tip' => 'Enter the LDAP group in the directory where allowed user logins are stored. Example: cn=sysadmins,ou=groups,dc=example,dc=com', 'server_password_change_enable' => 'Enable changing password on distant server (using ssh connection)', diff --git a/includes/language/estonian.php b/includes/language/estonian.php index 5f34a9fcb..45de97594 100755 --- a/includes/language/estonian.php +++ b/includes/language/estonian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/french.php b/includes/language/french.php index 4cc234e36..4a9c957a6 100755 --- a/includes/language/french.php +++ b/includes/language/french.php @@ -881,6 +881,10 @@ 'syslog_port' => 'Port Syslog', 'error_bad_credentials' => 'Informations de connexion erronées', 'bruteforce_wait' => 'Trop de tentatives échouées, votre compte est bloqué jusqu'à : ', + 'bruteforce_unlock_at' => 'Déblocage du compte (anti bruteforce) : ', + 'bruteforce_reset_account' => 'Réinitialiser l'anti bruteforce de l'utilisateur', + 'bruteforce_reset_mail_subject' => 'TEAMPASS - Votre compte est désactivé', + 'bruteforce_reset_mail_body' => 'Bonjour #name#,

Votre compte teampass a été verouillé en raison d'un grand nombre d'échecs d'authentification.

Vous pouvez le débloquer en cliquant sur ce lien #reset_url#

Déblocage automatique : #unlock_at#', 'settings_ldap_usergroup' => 'Groupe LDAP dans lequel faire la recherche', 'settings_ldap_usergroup_tip' => 'Groupe LDAP dans lequel les utilisateurs doivent être membre pour pouvoir se connecter. Exemple : cn=sysadmins,ou=groups,dc=example,dc=com', 'server_password_change_enable' => 'Activer le changement automatique du mot de passe du compte du serveur (en utilisant une connexion SSH)', @@ -1189,7 +1193,6 @@ 'show_item_data_tip' => 'Permet d'afficher des informations supplémentaires dans la liste des éléments (nom d'utilisateur, e-mail et URL). Cela peut être utile pour avoir un aperçu rapide du contenu de l'élément.', 'items_page_split_view_mode' => 'Afficher le détail d'un objet sur la page des objets', 'replace_tenant_id' => 'Adapter l'url tout en laissant {tenant-id}. Il sera remplacer lors de l'appel.', - 'user_exists_but_not_oauth2' => 'Vous devez vous authentifier avec votre compte Azure/Entra AD', 'user_not_allowed_to_auth_to_teampass_app' => 'Vous n'êtes pas autoriser à vous authentifier à l'application Teampass', 'user_is_not_auth_with_oauth2' => 'Vous ne pouvez pas vous authentifier avec un compte Entra/Azure AD', 'highlight_favorites' => 'Mettre en évidence les favoris', diff --git a/includes/language/german.php b/includes/language/german.php index a53a5cd46..d460cf735 100755 --- a/includes/language/german.php +++ b/includes/language/german.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Zeigt zusätzliche Informationen in der Eintragsliste an (Benutzername, E-Mail, URL). Dies erlaubt einen schnellen Überblick über den Inhalt der Einträge.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/greek.php b/includes/language/greek.php index 07711acaa..6a94f056c 100755 --- a/includes/language/greek.php +++ b/includes/language/greek.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/hungarian.php b/includes/language/hungarian.php index 0331904f6..eea0f226c 100755 --- a/includes/language/hungarian.php +++ b/includes/language/hungarian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/italian.php b/includes/language/italian.php index 151c2d130..388542f3c 100755 --- a/includes/language/italian.php +++ b/includes/language/italian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/japanese.php b/includes/language/japanese.php index 0529a7cc9..6750c6d64 100755 --- a/includes/language/japanese.php +++ b/includes/language/japanese.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/norwegian.php b/includes/language/norwegian.php index 5f9ef5bc4..34c875587 100755 --- a/includes/language/norwegian.php +++ b/includes/language/norwegian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/polish.php b/includes/language/polish.php index 78e432d11..f283bd484 100755 --- a/includes/language/polish.php +++ b/includes/language/polish.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/portuguese.php b/includes/language/portuguese.php index abea07d58..94671b22d 100755 --- a/includes/language/portuguese.php +++ b/includes/language/portuguese.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/portuguese_br.php b/includes/language/portuguese_br.php index c47a7f13e..0a0edaba7 100755 --- a/includes/language/portuguese_br.php +++ b/includes/language/portuguese_br.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/romanian.php b/includes/language/romanian.php index 469a87866..af8905072 100755 --- a/includes/language/romanian.php +++ b/includes/language/romanian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/russian.php b/includes/language/russian.php index 2d72730e8..897e11092 100755 --- a/includes/language/russian.php +++ b/includes/language/russian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/spanish.php b/includes/language/spanish.php index 993af2ae3..d4139ea21 100755 --- a/includes/language/spanish.php +++ b/includes/language/spanish.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permite mostrar información adicional en la lista de elementos (nombre de usuario, correo electrónico y URL). Esto podría ser útil para tener una vista rápida del contenido del elemento.', 'items_page_split_view_mode' => 'Mostrar los detalles del elemento en modo de vista dividida de página', 'replace_tenant_id' => 'Adapte la URL pero mantenga {tenant-id} tal cual. Se reemplazará en vivo por el ID del inquilino.', - 'user_exists_but_not_oauth2' => 'El usuario debe autenticarse utilizando Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'El usuario no tiene permitido autenticarse en la aplicación Teampass', 'user_is_not_auth_with_oauth2' => 'El usuario no debe autenticarse con Entra/Azure AD', 'highlight_favorites' => 'Resaltar favoritos', diff --git a/includes/language/swedish.php b/includes/language/swedish.php index 67a64668d..4943010dc 100755 --- a/includes/language/swedish.php +++ b/includes/language/swedish.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/turkish.php b/includes/language/turkish.php index 1270f4658..54cf1a77f 100755 --- a/includes/language/turkish.php +++ b/includes/language/turkish.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/includes/language/ukrainian.php b/includes/language/ukrainian.php index 413d48b4b..7bd869593 100755 --- a/includes/language/ukrainian.php +++ b/includes/language/ukrainian.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Дозволяє відображати додаткову інформацію у списку елементів (ім'я користувача, електронна пошта та URL). Це може бути корисно для швидкого перегляду вмісту елемента.', 'items_page_split_view_mode' => 'Показати деталі елемента у режимі розділеного перегляду сторінки', 'replace_tenant_id' => 'Адаптувати URL, але залишити {tenant-id} як є. Він буде замінений в реальному часі на ID орендаря.', - 'user_exists_but_not_oauth2' => 'Користувач повинен автентифікуватися за допомогою Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'Користувачеві не дозволено автентифікуватися за допомогою додатку Teampass', 'user_is_not_auth_with_oauth2' => 'Користувач не повинен автентифікуватися за допомогою Entra/Azure AD', 'highlight_favorites' => 'Виділити обрані', diff --git a/includes/language/vietnamese.php b/includes/language/vietnamese.php index bcf5a19d9..950ae615d 100755 --- a/includes/language/vietnamese.php +++ b/includes/language/vietnamese.php @@ -1188,7 +1188,6 @@ 'show_item_data_tip' => 'Permits to display extra information in the items list (username, email and url). This could be useful to have a quick view of the item content.', 'items_page_split_view_mode' => 'Show item details in page split view mode', 'replace_tenant_id' => 'Adapt the URL but keep {tenant-id} as is. It will be replaced live by the tenant ID.', - 'user_exists_but_not_oauth2' => 'User has to authenticate using Entra/Azure AD', 'user_not_allowed_to_auth_to_teampass_app' => 'User is not allowed to authenticate with Teampass application', 'user_is_not_auth_with_oauth2' => 'User should not authenticate with Entra/Azure AD', 'highlight_favorites' => 'Highlight favorites', diff --git a/index.php b/index.php index d2da97719..9a920ebc8 100755 --- a/index.php +++ b/index.php @@ -92,6 +92,7 @@ // Quick major version check -> upgrade needed? if (isset($SETTINGS['teampass_version']) === true && version_compare(TP_VERSION, $SETTINGS['teampass_version']) > 0) { + $session->invalidate(); // Perform redirection if (headers_sent()) { echo ''; diff --git a/install/install.queries.php b/install/install.queries.php index fda222154..5358990ad 100755 --- a/install/install.queries.php +++ b/install/install.queries.php @@ -1470,6 +1470,7 @@ function encryptFollowingDefuse($message, $ascii_key) `value` VARCHAR(500) NOT NULL, `date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `unlock_at` TIMESTAMP NULL DEFAULT NULL, + `unlock_code` VARCHAR(50) NULL DEFAULT NULL, PRIMARY KEY (`id`) ) CHARSET=utf8;" ); diff --git a/install/upgrade_run_3.1.php b/install/upgrade_run_3.1.php index 12cc11126..dba4be0af 100755 --- a/install/upgrade_run_3.1.php +++ b/install/upgrade_run_3.1.php @@ -626,10 +626,24 @@ `value` VARCHAR(500) NOT NULL, `date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `unlock_at` TIMESTAMP NULL DEFAULT NULL, + `unlock_code` VARCHAR(50) NULL DEFAULT NULL, PRIMARY KEY (`id`) ) CHARSET=utf8;" ); +// Add unlock_code column +try { + $alter_table_query = " + ALTER TABLE `" . $pre . "auth_failures` + ADD COLUMN `unlock_code` VARCHAR(50) NULL DEFAULT NULL;"; + mysqli_begin_transaction($db_link); + mysqli_query($db_link, $alter_table_query); + mysqli_commit($db_link); +} catch (Exception $e) { + // Rollback transaction if index already exists. + mysqli_rollback($db_link); +} + //--- ' + percentage + '%'; - //console.log(message) $('#onthefly-restore-progress-text').text(percentage); } @@ -314,13 +312,14 @@ function updateProgressBar(offset, totalSize) { : $SETTINGS['upload_maxfilesize']; ?> + let toastrElement; var restoreOperationId = '', uploader_restoreDB = new plupload.Uploader({ runtimes: "gears,html5,flash,silverlight,browserplus", browse_button: "onthefly-restore-file-select", container: "onthefly-restore-file", max_file_size: "", - chunk_size: "5mb", + chunk_size: "2mb", // adapted to standard PHP configuration unique_names: true, dragdrop: true, multiple_queues: false, @@ -364,8 +363,7 @@ function(teampassUser) { BeforeUpload: function(up, file) { // Show cog toastr.remove(); - toastr.info('get('loading_item'); ?> ... '); - console.log("Upload token: "+store.get('teampassUser').uploadToken); + toastrElement = toastr.info('get('loading_item'); ?> ... 0%'); up.setOption('multipart_params', { PHPSESSID: 'get('user-id'); ?>', @@ -374,6 +372,10 @@ function(teampassUser) { user_token: store.get('teampassUser').uploadToken }); }, + UploadProgress: function(up, file) { + // Update only the percentage inside the Toastr message + $('#plupload-progress').text(file.percent + '%'); + }, UploadComplete: function(up, files) { store.update( 'teampassUser', diff --git a/pages/import.js.php b/pages/import.js.php index 0ea5af7af..f1e76c725 100755 --- a/pages/import.js.php +++ b/pages/import.js.php @@ -142,21 +142,8 @@ function(teampassApplication) { PHPSESSID: 'get('key'); ?>', type_upload: "import_items_from_csv", user_token: data[0].token - /*itemId: store.get('teampassItem').id, - type_upload: 'item_attachments', - isNewItem: store.get('teampassItem').isNewItem, - isPersonal: store.get('teampassItem').folderIsPersonal, - edit_item: false, - user_upload_token: store.get('teampassApplication').attachmentToken, - randomId: store.get('teampassApplication').uploadedFileId, - files_number: $('#form-item-hidden-pickFilesNumber').val(), - file_size: file.size*/ }); - /*up.settings.multipart_params.PHPSESSID = ""; - up.settings.multipart_params.type_upload = "import_items_from_csv"; - up.settings.multipart_params.user_token = data[0].token;*/ - up.start(); }, "json" diff --git a/pages/items.js.php b/pages/items.js.php index 1fbd56a72..213df1bf4 100755 --- a/pages/items.js.php +++ b/pages/items.js.php @@ -383,14 +383,13 @@ function(teampassApplication) { $(document).on('click', '#card-item-pwd-show-button', function() { if ($(this).hasClass('pwd-shown') === false) { $(this).addClass('pwd-shown'); - // Prepare data to show - // Is data crypted? - var data = unCryptData($('#hidden-item-pwd').val(), 'get('key'); ?>'); - if (data !== false && data !== undefined) { - $('#hidden-item-pwd').val( - data.password - ); - } + + // Get item password from server + const item_pwd = getItemPassword( + 'at_password_shown', + 'item_id', + store.get('teampassItem').id + ); // Change class and show spinner $('.pwd-show-spinner') @@ -399,16 +398,9 @@ function(teampassApplication) { // display raw password $('#card-item-pwd') - .text($('#hidden-item-pwd').val()) + .text(item_pwd) .addClass('pointer_none'); - // log password is shown - itemLog( - 'at_password_shown', - store.get('teampassItem').id, - $('#card-item-label').text() - ); - // Autohide setTimeout(() => { $(this).removeClass('pwd-shown'); @@ -427,6 +419,22 @@ function(teampassApplication) { // Manage folders action $('.tp-action').click(function() { + // Ensure that the local storage data is consistent with what is + // displayed on the screen. + const item_dom_id = parseInt($('#items-details-container').data('id')); + const item_storage_id = parseInt(store.get('teampassItem').id); + if (item_dom_id !== item_storage_id) { + toastr.remove(); + toastr.error( + 'get('data_inconsistency'); ?>', + '', { + timeOut: 5000, + progressBar: true + } + ); + return false; + } + // SHow user toastr.remove(); toastr.info('get('in_progress'); ?>'); @@ -766,6 +774,7 @@ function(teampassItem) { // > END < // } else if ($(this).data('item-action') === 'edit') { + const item_tree_id = store.get('teampassItem').tree_id; if (debugJavascript === true) console.info('SHOW EDIT ITEM'); // Reset item store.update( @@ -790,7 +799,7 @@ function(teampassItem) { } $.when( - getPrivilegesOnItem(selectedFolderId, 1) + getPrivilegesOnItem(item_tree_id, 1) ).then(function(retData) { console.log('getPrivilegesOnItem 2') console.log(retData) @@ -838,7 +847,7 @@ function(teampassItem) { $('#form-item').removeClass('was-validated'); // Now manage edtion - showItemEditForm(selectedFolderId); + showItemEditForm(item_tree_id); }); // @@ -2581,34 +2590,24 @@ function(ret) { mouseStillDown = false; showPwdContinuous(); }); - var showPwdContinuous = function() { - if (mouseStillDown === true) { - // Prepare data to show - // Is data crypted? - var data = unCryptData($('#hidden-item-pwd').val(), 'get('key'); ?>'); - if (data !== false && data !== undefined) { - $('#hidden-item-pwd').val( - data.password - ); - } - $('#card-item-pwd') - .html( - // XSS Filtering - $('').text($('#hidden-item-pwd').val()).html() - ); + const showPwdContinuous = function() { + if (mouseStillDown === true + && !$('#card-item-pwd').hasClass('pwd-shown')) { + + // Get item password from server + const item_pwd = getItemPassword( + 'at_password_shown', + 'item_id', + store.get('teampassItem').id + ); + $('#card-item-pwd').text(item_pwd); + $('#card-item-pwd').addClass('pwd-shown'); + + // Auto hide password setTimeout('showPwdContinuous("card-item-pwd")', 50); - // log password is shown - if ($('#card-item-pwd').hasClass('pwd-shown') === false) { - itemLog( - 'at_password_shown', - store.get('teampassItem').id, - $('#card-item-label').text() - ); - $('#card-item-pwd').addClass('pwd-shown'); - } - } else { + } else if(mouseStillDown !== true) { $('#card-item-pwd') .html('') .removeClass('pwd-shown'); @@ -2760,6 +2759,8 @@ function(ret) { var mime_types = ; var prevent_empty = ; var resize = ; + let toastrElement; + let fileId; var uploader_attachments = new plupload.Uploader({ runtimes: 'html5,flash,silverlight,html4', @@ -2778,12 +2779,11 @@ function(ret) { resize: resize, init: { BeforeUpload: function(up, file) { - toastr.info( - 'get('uploading'); ?>', - '', { - timeOut: 0 - } - ); + fileId = file.id; + toastr.remove(); + toastrElement = toastr.info('get('loading_item'); ?> ... 0%'); + // Show file name + $('#upload-file_' + file.id).html('' + htmlEncode(file.name) + ''); // Get random number if (store.get('teampassApplication').uploadedFileId === '') { @@ -2807,17 +2807,23 @@ function(teampassApplication) { files_number: $('#form-item-hidden-pickFilesNumber').val(), file_size: file.size }); + }, + UploadProgress: function(up, file) { + // Update only the percentage inside the Toastr message + $('#plupload-progress').text(file.percent + '%'); + }, + UploadComplete: function(up, files) { + // Inform user + toastr.remove(); + }, + Error: function(up, args) { + console.log("ERROR arguments:"); + console.log(args); } } }); - // Uploader options - uploader_attachments.bind('UploadProgress', function(up, file) { - //console.log('uploader_attachments.bind') - $('#upload-file_' + file.id).html('' + htmlEncode(file.name) + ''); - }); uploader_attachments.bind('FileUploaded', function(up, file) { - //console.log('File '+file.name+' uploaded'); $('#fileStatus_'+file.id).html(''); userUploadedFile = true; userDidAChange = true; @@ -2825,15 +2831,39 @@ function(teampassApplication) { }); uploader_attachments.bind('Error', function(up, err) { toastr.remove(); - toastr.error( - err.message + (err.file ? ', File: ' + err.file.name : ''), - '', { - timeOut: 5000, - progressBar: true + // Extraire le message d'erreur + let errorMessage = 'An unknown error occurred.'; + if (err.response) { + try { + const response = JSON.parse(err.response); + if (response.error && response.error.message) { + errorMessage = response.error.message; + } + } catch (e) { + errorMessage = err.response; // Si la réponse n'est pas JSON } - ); + } - up.refresh(); // Reposition Flash/Silverlight + // Vérifie si l'erreur est due à un dépassement de taille ou une autre erreur critique + if (err.code === -200 || err.status === 413) { + // Arrêter l'upload des chunks + up.stop(); + errorMessage += ' - Upload stopped.'; + + // Affiche l'erreur dans l'interface utilisateur + toastr.error( + errorMessage + (err.file ? ', File: ' + err.file.name : ''), + '', { + timeOut: 10000, + progressBar: true + } + ); + + $('#fileStatus_'+fileId).html(''); + return false; + } else { + up.refresh(); // Reposition Flash/Silverlight + } }); $("#form-item-upload-pickfiles").click(function(e) { @@ -3380,6 +3410,14 @@ function(teampassItem) { } ); } else { + // Get password and fill the field. + const item_pwd = getItemPassword( + 'at_password_shown_edit_form', + 'item_id', + store.get('teampassItem').id + ); + $('#form-item-password').val(item_pwd); + $('#card-item-visibility').html(store.get('teampassItem').itemVisibility); $('#card-item-minimum-complexity').html(store.get('teampassItem').itemMinimumComplexity); @@ -3980,57 +4018,18 @@ function(teampassItem) { // Send query and get password var result = '', error = false; - - $.ajax({ - type: "POST", - async: false, - url: 'sources/items.queries.php', - data: 'type=show_item_password&item_key=' + trigger.getAttribute('data-item-key') + - '&key=get('key'); ?>', - dataType: "", - success: function(data) { - //decrypt data - try { - data = prepareExchangedData(data, "decode", "get('key'); ?>"); - } catch (e) { - // error - toastr.remove(); - toastr.warning( - 'get('no_item_to_display'); ?>' - ); - return false; - } - if (data.error === true) { - error = true; - } else { - if (data.password_error !== '') { - error = true; - } else { - result = simplePurifier(atob(data.password), false, false, false, false).utf8Decode(); - } - if (result === '') { - toastr.info( - 'get('password_is_empty'); ?>', - '', { - timeOut: 2000, - positionClass: 'toast-bottom-right', - progressBar: true - } - ); - } - } - } - }); - return result; + + // Get item password from server + const item_pwd = getItemPassword( + 'at_password_copied', + 'item_key', + trigger.getAttribute('data-item-key') + ); + + return item_pwd; } }); - clipboardForPassword.on('success', function(e) { - itemLog( - 'at_password_copied', - e.trigger.dataset.itemId, - e.trigger.dataset.itemLabel - ); - + clipboardForPassword.on('success', function(e) { // Warn user about clipboard clear if (store.get('teampassSettings').clipboard_life_duration === undefined || parseInt(store.get('teampassSettings').clipboard_life_duration) === 0) { toastr.remove(); @@ -4338,10 +4337,10 @@ function(teampassApplication) { // Show user that password is badly encrypted (value.pw_status === 'encryption_error' ? '' : '') + // Prepare item info + '' + '' + // Show item fa_icon if set (value.fa_icon !== '' ? '' : '') + - '' + '' + value.label + '' + (value.rights === 10 ? '' : description) + '' + '' + @@ -4834,16 +4833,12 @@ function(teampassUser) { $('#card-item-pwd').after(''); } - // Uncrypt the pwd - if (data.pw !== undefined) { - data.pw = simplePurifier(atob(data.pw), false, false, false, false).utf8Decode(); - } - // Update hidden variables store.update( 'teampassItem', function(teampassItem) { teampassItem.id = parseInt(data.id), + teampassItem.tree_id = parseInt(data.folder), teampassItem.folderId = parseInt(data.folder), teampassItem.timestamp = data.timestamp, teampassItem.user_can_modify = data.user_can_modify, @@ -4901,7 +4896,11 @@ function(teampassItem) { $('.form-item').removeClass('hidden'); $('#folders-tree-card').addClass('hidden'); } - $('#pwd-definition-size').val(data.pw.length); + $('#pwd-definition-size').val(data.pw_length); + + // Store current item id in the DOM (cannot be updated in + // an other tab or window) + $('#items-details-container').data('id', data.id); // Prepare card const itemIcon = (data.fa_icon !== "") ? '' : ''; @@ -4914,8 +4913,6 @@ function(teampassItem) { $('#card-item-description').removeClass('hidden'); } $('#card-item-pwd').html(''); - $('#hidden-item-pwd, #form-item-suggestion-password').val(data.pw); - $('#form-item-password, #form-item-password-confirmation, #form-item-server-old-password').val(data.pw); $('#card-item-login').html(data.login); $('#form-item-login, #form-item-suggestion-login, #form-item-server-login').val(data.login); @@ -5179,7 +5176,7 @@ function(teampassItem) { } // Prepare clipboard - COPY PASSWORD - if (data.pw !== '' && store.get('teampassItem').readyToUse === true) { + if (data.pw_length > 0 && store.get('teampassItem').readyToUse === true) { // Delete existing clipboard if (clipboardForPasswordListItems) { clipboardForPasswordListItems.destroy(); @@ -5187,16 +5184,17 @@ function(teampassItem) { // New clipboard clipboardForPasswordListItems = new ClipboardJS('#card-item-pwd-button', { text: function() { - return (data.pw); + // Get item password from server + const item_pwd = getItemPassword( + 'at_password_copied', + 'item_id', + data.id + ); + + return item_pwd; } }) .on('success', function(e) { - itemLog( - 'at_password_copied', - store.get('teampassItem').id, - $('#card-item-label').text() - ); - // Warn user about clipboard clear if (store.get('teampassSettings').clipboard_life_duration === undefined || parseInt(store.get('teampassSettings').clipboard_life_duration) === 0) { toastr.remove(); @@ -6427,10 +6425,6 @@ function(data) { ); }); - $('#item-button-password-copy').click(function() { - $('#form-item-password-confirmation').val($('#form-item-password').val()); - }); - /** * On tag badge click, launch the search query */ diff --git a/pages/profile.js.php b/pages/profile.js.php index 158a8f67c..1309ccb5b 100755 --- a/pages/profile.js.php +++ b/pages/profile.js.php @@ -567,11 +567,20 @@ function(data) { event.preventDefault(); $('#dialog-recovery-keys-download').removeClass('hidden'); + // Default text on dialog box + let dialog_content = 'get('download_recovery_keys_confirmation'); ?>' + + // Request authentication on local and ldap accounts + if (store.get('teampassUser').auth_type !== 'oauth2') { + dialog_content += '

get('confirm_password'); ?>' + + ''; + } + // Prepare modal showModalDialogBox( '#warningModal', 'get('caution'); ?>', - 'get('download_recovery_keys_confirmation'); ?>', + dialog_content, 'get('download'); ?>', 'get('close'); ?>', false, @@ -583,12 +592,25 @@ function(data) { $(document).on('click', '#warningModalButtonAction', function(event) { event.preventDefault(); + // Ensure that a password is provided by user + const user_pasword = $('#keys-download-confirm-pwd').val() ?? ''; + if (store.get('teampassUser').auth_type !== 'oauth2' && !user_pasword) { + toastr.remove(); + toastr.error( + 'get('password_cannot_be_empty'); ?>', + 'get('caution'); ?>', { + timeOut: 5000, + progressBar: true + } + ); + return false; + } + if (RequestOnGoing === true) { return false; } RequestOnGoing = true; - // We have the password, start reencryption $('#warningModalButtonAction') .addClass('disabled') .html(''); @@ -598,12 +620,16 @@ function(data) { toastr.remove(); toastr.info('get('in_progress'); ?>'); + let data = { + password: user_pasword, + }; // Do query $.post( "sources/main.queries.php", { 'type': "user_recovery_keys_download", 'type_category': 'action_key', - 'key': 'get('key'); ?>' + 'key': 'get('key'); ?>', + 'data': prepareExchangedData(JSON.stringify(data), "encode", "get('key'); ?>"), }, function(data) { data = prepareExchangedData(data, "decode", "get('key'); ?>"); @@ -620,8 +646,11 @@ function(data) { ); // Enable buttons - $("#user-current-defuse-psk-progress").html('get('provide_current_psk_and_click_launch'); ?>'); - $('#button_do_sharekeys_reencryption, #button_close_sharekeys_reencryption').removeAttr('disabled'); + $('#warningModalButtonAction') + .removeClass('disabled') + .html('get('download'); ?>'); + RequestOnGoing = false; + return false; } else { $('#profile-keys_download-date').text(data.datetime); diff --git a/pages/profile.php b/pages/profile.php index b3d03ff0c..21ee4ad85 100755 --- a/pages/profile.php +++ b/pages/profile.php @@ -466,25 +466,6 @@ - -
- -
- -
-
- -
diff --git a/pages/search.js.php b/pages/search.js.php index 0644f0a91..a85f3e45a 100755 --- a/pages/search.js.php +++ b/pages/search.js.php @@ -251,7 +251,6 @@ function(data) { '' + '' + '' + - '' + '' + '
' + (data.login === '' ? '' : @@ -292,18 +291,18 @@ function(data) { // Manage buttons --> PASSWORD new ClipboardJS('#btn-copy-pwd', { - text: function(trigger) { - // Read password - return $('#pwd-hidden_' + $('#btn-copy-pwd').data('id')).val(); + text: function(trigger) { + // Get item password from server + const item_pwd = getItemPassword( + 'at_password_copied', + 'item_id', + $('#btn-copy-pwd').data('id') + ); + + return item_pwd; } }) .on('success', function(e) { - itemLog( - 'at_password_copied', - e.trigger.dataset.id, - e.trigger.dataset.label - ); - // Warn user about clipboard clear if (store.get('teampassSettings').clipboard_life_duration === undefined || parseInt(store.get('teampassSettings').clipboard_life_duration) === 0) { toastr.remove(); @@ -381,8 +380,9 @@ function(data) { } // show password during longpress - var mouseStillDown = false; - $('#search-results-items').on('mousedown', '.unhide_masked_data', function(event) { + let mouseStillDown = false; + $('#search-results-items') + .on('mousedown', '.unhide_masked_data', function(event) { mouseStillDown = true; showPwdContinuous($(this).attr('id')); @@ -396,37 +396,23 @@ function(data) { showPwdContinuous($(this).attr('id')); }); - var showPwdContinuous = function(elem_id) { - var itemId = elem_id.split('_')[1]; - if (mouseStillDown === true) { - // Prepare data to show - // Is data crypted? - var data = unCryptData($('#pwd-hidden_' + itemId).val(), 'get('key'); ?>'); - - if (data !== false) { - $('#pwd-hidden_' + itemId).val( - data.password - ); - } + const showPwdContinuous = function showPwdContinuous(elem_id) { + const itemId = elem_id.split('_')[1]; + if (mouseStillDown === true + && !$('#pwd-show_' + itemId).hasClass('pwd-shown')) { - $('#pwd-show_' + itemId).html( - '' + - $('#pwd-hidden_' + simplePurifier(itemId).val(), false, false, false, false) + - '' + const item_pwd = getItemPassword( + 'at_password_shown', + 'item_id', + itemId ); + $('#pwd-show_' + itemId).text(item_pwd); + $('#pwd-show_' + itemId).addClass('pwd-shown'); + + // Auto hide password setTimeout('showPwdContinuous("pwd-show_' + itemId + '")', 50); - - // log password is shown - if ($("#pwd-show_" + itemId).hasClass('pwd-shown') === false) { - itemLog( - 'at_password_shown', - itemId, - $('#pwd-label_' + itemId).text() - ); - $('#pwd-show_' + itemId).addClass('pwd-shown'); - } - } else { + } else if(mouseStillDown !== true) { $('#pwd-show_' + itemId) .html('') .removeClass('pwd-shown'); @@ -437,35 +423,22 @@ function(data) { // including autohide after a couple of seconds $(document).on('click', '.btn-show-pwd', function() { if ($(this).hasClass('pwd-shown') === false) { - var itemId = $(this).data('id'); + const itemId = $(this).data('id'); $(this).addClass('pwd-shown'); - // Prepare data to show - // Is data crypted? - var data = unCryptData($('#pwd-hidden_' + itemId).val(), 'get('key'); ?>'); - if (data !== false && data !== undefined) { - $('#pwd-hidden_' + itemId).val( - data.password - ); - } + const item_pwd = getItemPassword( + 'at_password_shown', + 'item_id', + itemId + ); + + $('#pwd-show_' + itemId).text(item_pwd); + // Change class and show spinner $('.pwd-show-spinner') .removeClass('far fa-eye') .addClass('fas fa-circle-notch fa-spin text-warning'); - - $('#pwd-show_' + itemId) - .html( - $('').text($('#pwd-hidden_' + itemId).val()).html() - ); - - // log password is shown - itemLog( - 'at_password_shown', - itemId, - $('#item-label').text() - ); - // Autohide setTimeout(() => { $(this).removeClass('pwd-shown'); diff --git a/pages/users.js.php b/pages/users.js.php index 7b04e8253..9dbe736d2 100755 --- a/pages/users.js.php +++ b/pages/users.js.php @@ -159,6 +159,7 @@ className: 'details-control', '' : '' ) + + '' + '' + '' + '' + @@ -1182,7 +1183,35 @@ function(data) { } ); - // --- + } else if ($(this).data('action') === 'reset-antibruteforce') { + toastr.remove(); + toastr.info('get('in_progress'); ?> ... '); + + const data = { + 'user_id': $(this).data('id'), + }; + + $.post( + "sources/users.queries.php", { + type: "reset_antibruteforce", + data: prepareExchangedData(JSON.stringify(data), 'encode', 'get('key'); ?>'), + key: "get('key'); ?>" + }, + function(data) { + // Inform user + toastr.remove(); + toastr.success( + 'get('done'); ?>', + '', { + timeOut: 1000 + } + ); + + // refresh table content + oTable.ajax.reload(); + } + ); + } else if ($(this).data('action') === 'new-enc-code') { // HIde $('.content-header, .content').addClass('hidden'); @@ -1268,7 +1297,7 @@ function(data) { $('input[type="checkbox"].flat-blue').iCheck({ checkboxClass: 'icheckbox_flat-blue', }); - $(document).on('click', '#warningModalButtonAction', function() { + $(document).one('click', '#warningModalButtonAction', function() { // Show spinner toastr.remove(); @@ -1344,7 +1373,7 @@ function(data) { $('input[type="checkbox"].flat-blue').iCheck({ checkboxClass: 'icheckbox_flat-blue', }); - $(document).on('click', '#warningModalButtonAction', function() { + $(document).one('click', '#warningModalButtonAction', function() { if ($('#user-to-delete').prop('checked') === false) { $('#warningModal').modal('hide'); return false; @@ -2443,7 +2472,7 @@ function(data) { 'get('perform'); ?>', 'get('cancel'); ?>' ); - $(document).on('click', '#warningModalButtonAction', function(event) { + $(document).one('click', '#warningModalButtonAction', function(event) { event.preventDefault(); event.stopPropagation(); if ($('#ldap-user-name').val() !== "" && $('#ldap-user-roles :selected').length > 0) { diff --git a/scripts/background_tasks___items_handler.php b/scripts/background_tasks___items_handler.php index a15b260db..93f5a9ef6 100755 --- a/scripts/background_tasks___items_handler.php +++ b/scripts/background_tasks___items_handler.php @@ -130,18 +130,42 @@ // log end doLog('end', '', (isset($SETTINGS['enable_tasks_log']) === true ? (int) $SETTINGS['enable_tasks_log'] : 0), $logID); -// launch a new iterative process -$process_to_perform = DB::queryfirstrow( - 'SELECT * - FROM ' . prefixTable('background_tasks') . ' - WHERE is_in_progress = %i AND process_type IN ("item_copy", "new_item", "update_item", "item_update_create_keys") - ORDER BY increment_id DESC', - 1 -); -if (DB::count() > 0) { - $process = new Symfony\Component\Process\Process([$phpBinaryPath, __FILE__]); - $process->start(); - $process->wait(); +// The main process run new iteratives process for each subtask +if (!in_array('--child', $argv)) { + // Save subtasks start time + $start_time = time(); + + // Run new subtasks until there are no more to handle or we have exceeded + // the execution minute (the next execution will continue) + do { + // Search if there are remaining tasks + $process_to_perform = DB::queryFirstField( + 'SELECT 1 + FROM ' . prefixTable('background_tasks') . ' + WHERE is_in_progress = %i AND process_type IN ( + "item_copy", + "new_item", + "update_item", + "item_update_create_keys" + ) + ORDER BY increment_id DESC LIMIT 1', + 1 + ); + + // No more tasks, exit + if ($process_to_perform !== 1) + break; + + // Run next task + $process = new Symfony\Component\Process\Process([ + $phpBinaryPath, + __FILE__, + '--child' + ]); + $process->start(); + $process->wait(); + + } while (time() - $start_time < 60); } @@ -171,99 +195,99 @@ function handleTask(int $processId, array $ProcessArguments, array $SETTINGS, in if (DB::count() > 0) { // check if a linux process is not currently on going // if sub_task_in_progress === 1 then exit - if ((int) $task_to_perform['sub_task_in_progress'] === 0) { - // handle next task - $args = json_decode($task_to_perform['task'], true); - provideLog('[TASK][#'. $task_to_perform['increment_id'].'][START]Task '.$args['step'], $SETTINGS); + if ((int) $task_to_perform['sub_task_in_progress'] !== 0) { + // Task is currently being in progress by another server process + provideLog('[TASK][#'. $task_to_perform['increment_id'].'][WARNING] Similar task already being processes', $SETTINGS); + return false; + } - // flag as in progress - DB::update( - prefixTable('background_tasks'), - array( - 'updated_at' => time(), - 'is_in_progress' => 1, - ), - 'increment_id = %i', - $processId - ); + // handle next task + $args = json_decode($task_to_perform['task'], true); + provideLog('[TASK][#'. $task_to_perform['increment_id'].'][START]Task '.$args['step'], $SETTINGS); - // flag task as on going - if ((int) $args['index'] === 0) { - DB::update( - prefixTable('background_subtasks'), - array( - 'is_in_progress' => 1, - ), - 'increment_id = %i', - $task_to_perform['increment_id'] - ); - } + // flag as in progress + DB::update( + prefixTable('background_tasks'), + array( + 'updated_at' => time(), + 'is_in_progress' => 1, + ), + 'increment_id = %i', + $processId + ); - // flag sub task in progress as on going + // flag task as on going + if ((int) $args['index'] === 0) { DB::update( prefixTable('background_subtasks'), array( - 'sub_task_in_progress' => 1, + 'is_in_progress' => 1, ), 'increment_id = %i', $task_to_perform['increment_id'] ); + } - // handle the task step - handleTaskStep($args, $ProcessArguments, $SETTINGS); + // flag sub task in progress as on going + DB::update( + prefixTable('background_subtasks'), + array( + 'sub_task_in_progress' => 1, + ), + 'increment_id = %i', + $task_to_perform['increment_id'] + ); - // update the task status + // handle the task step + handleTaskStep($args, $ProcessArguments, $SETTINGS); + + // update the task status + DB::update( + prefixTable('background_subtasks'), + array( + 'sub_task_in_progress' => 0, // flag sub task is no more in prgoress + 'task' => json_encode(["status" => "Done"]), + 'is_in_progress' => -1, + 'finished_at' => time(), + 'updated_at' => time(), + ), + 'increment_id = %i', + $task_to_perform['increment_id'] + ); + + provideLog('[TASK]['.$args['step'].'] starting at '.$args['index'].' is done.', $SETTINGS); + + // are all tasks done? + DB::query( + 'SELECT * + FROM ' . prefixTable('background_subtasks') . ' + WHERE task_id = %i AND finished_at IS NULL', + $processId + ); + if (DB::count() === 0) { + // all tasks are done + provideLog('[PROCESS]['.$processId.'][FINISHED]', $SETTINGS); + DB::debugmode(false); DB::update( - prefixTable('background_subtasks'), + prefixTable('background_tasks'), array( - 'sub_task_in_progress' => 0, // flag sub task is no more in prgoress - 'task' => json_encode(["status" => "Done"]), - 'is_in_progress' => -1, 'finished_at' => time(), - 'updated_at' => time(), + 'is_in_progress' => -1, + 'arguments' => json_encode([ + 'new_user_id' => isset($ProcessArguments['new_user_id']) === true ? $ProcessArguments['new_user_id'] : '', + ]) ), 'increment_id = %i', - $task_to_perform['increment_id'] - ); - - provideLog('[TASK]['.$args['step'].'] starting at '.$args['index'].' is done.', $SETTINGS); - - // are all tasks done? - DB::query( - 'SELECT * - FROM ' . prefixTable('background_subtasks') . ' - WHERE task_id = %i AND finished_at IS NULL', $processId ); - if (DB::count() === 0) { - // all tasks are done - provideLog('[PROCESS]['.$processId.'][FINISHED]', $SETTINGS); - DB::debugmode(false); - DB::update( - prefixTable('background_tasks'), - array( - 'finished_at' => time(), - 'is_in_progress' => -1, - 'arguments' => json_encode([ - 'new_user_id' => isset($ProcessArguments['new_user_id']) === true ? $ProcessArguments['new_user_id'] : '', - ]) - ), - 'increment_id = %i', - $processId - ); - - // if item was being updated then remove the edition lock - if (is_null($itemId) === false) { - DB::delete(prefixTable('items_edition'), 'item_id = %i', $itemId); - } - } - return false; - } else { - // Task is currently being in progress by another server process - provideLog('[TASK][#'. $task_to_perform['increment_id'].'][WARNING] Similar task already being processes', $SETTINGS); - return false; + // if item was being updated then remove the edition lock + if (is_null($itemId) === false) { + DB::delete(prefixTable('items_edition'), 'item_id = %i', $itemId); + } } + return false; + } else { // no more task to perform provideLog('[PROCESS]['.$processId.'][FINISHED]', $SETTINGS); @@ -378,4 +402,4 @@ function performRecuringItemTasks($SETTINGS): void WHERE timestamp < %i', ($SETTINGS['delay_item_edition'] > 0) ? time() - ($SETTINGS['delay_item_edition']*60) : time() - EDITION_LOCK_PERIOD ); -} \ No newline at end of file +} diff --git a/self-unlock.php b/self-unlock.php new file mode 100644 index 000000000..0178aab8e --- /dev/null +++ b/self-unlock.php @@ -0,0 +1,81 @@ +. + * + * Certain components of this file may be under different licenses. For + * details, see the `licenses` directory or individual file headers. + * --- + * @file 2fa.js.php + * @author Nils Laumaillé (nils@teampass.net) + * @copyright 2009-2024 Teampass.net + * @license GPL-3.0 + * @see https://www.teampass.net + */ + + +use Symfony\Component\HttpFoundation\Request as SymfonyRequest; + +// Load functions +require_once __DIR__. '/includes/config/include.php'; +require_once __DIR__.'/sources/main.functions.php'; + +// init +loadClasses(); + +// Get username and OTP from GET parameters +$request = SymfonyRequest::createFromGlobals(); +$username = $request->query->get('login', ''); +$otp = $request->query->get('otp', ''); + +// Redirect user to teampass if username or otp is not provided +if (empty($username) || empty($otp)) { + header('Location: ./index.php'); + exit; +} + +// Check for existing lock +$result = DB::queryFirstField( + 'SELECT 1 + FROM ' . prefixTable('auth_failures') . ' + WHERE unlock_at = ( + SELECT MAX(unlock_at) + FROM ' . prefixTable('auth_failures') . ' + WHERE unlock_at > %s + AND source = %s AND value = %s) + AND unlock_code = %s', + date('Y-m-d H:i:s', time()), + 'login', + $username, + $otp +); + +// Delete all logs for this user if provided OTP is correct +if ($result) { + DB::delete( + prefixTable('auth_failures'), + 'source = %s AND value = %s', + 'login', + $username + ); +} + +// Redirect user to teampass +header('Location: ./index.php'); +exit; diff --git a/sources/admin.queries.php b/sources/admin.queries.php index b348ccf39..ff8e5840a 100755 --- a/sources/admin.queries.php +++ b/sources/admin.queries.php @@ -206,7 +206,6 @@ 'encrypt', $SETTINGS['path_to_files_folder'] . '/' . $filename, $SETTINGS['path_to_files_folder'] . '/defuse_temp_' . $filename, - $SETTINGS, $post_option ); @@ -283,7 +282,6 @@ 'decrypt', $SETTINGS['path_to_files_folder'] . '/' . $file, $SETTINGS['path_to_files_folder'] . '/defuse_temp_' . $file, - $SETTINGS, $key ); @@ -890,8 +888,7 @@ prepareFileWithDefuse( 'decrypt', $SETTINGS['path_to_upload_folder'] . '/' . $record['file'], - $SETTINGS['path_to_upload_folder'] . '/' . $record['file'] . '_encrypted', - $SETTINGS + $SETTINGS['path_to_upload_folder'] . '/' . $record['file'] . '_encrypted' ); // Do cleanup of files @@ -901,8 +898,7 @@ prepareFileWithDefuse( 'encryp', $SETTINGS['path_to_upload_folder'] . '/' . $record['file'] . '_encrypted', - $SETTINGS['path_to_upload_folder'] . '/' . $record['file'], - $SETTINGS + $SETTINGS['path_to_upload_folder'] . '/' . $record['file'] ); // Do cleanup of files @@ -1476,7 +1472,6 @@ 'decrypt', $SETTINGS['path_to_upload_folder'] . '/' . $file_info['file'], $SETTINGS['path_to_upload_folder'] . '/defuse_temp_' . $file_info['file'], - $SETTINGS ); // Case where we want to encrypt } elseif ($post_option === 'encrypt') { @@ -1484,7 +1479,6 @@ 'encrypt', $SETTINGS['path_to_upload_folder'] . '/' . $file_info['file'], $SETTINGS['path_to_upload_folder'] . '/defuse_temp_' . $file_info['file'], - $SETTINGS ); } // Do file cleanup diff --git a/sources/backups.queries.php b/sources/backups.queries.php index 12e118f16..02208a96d 100755 --- a/sources/backups.queries.php +++ b/sources/backups.queries.php @@ -173,7 +173,7 @@ while ($row = $result->fetch_row()) { $return .= 'INSERT INTO ' . $table . ' VALUES('; for ($j = 0; $j < $numFields; ++$j) { - // Gestion des valeurs NULL + // Manage NULL values $value = $row[$j] === null ? 'NULL' : '"' . addslashes(preg_replace("/\n/", '\\n', $row[$j])) . '"'; $return .= $value; if ($j < ($numFields - 1)) { @@ -199,7 +199,7 @@ fwrite($handle, $return); fclose($handle); } - + // Encrypt the file if (empty($post_key) === false) { // Encrypt the file @@ -207,7 +207,6 @@ 'encrypt', $SETTINGS['path_to_files_folder'] . '/' . $filename, $SETTINGS['path_to_files_folder'] . '/defuse_temp_' . $filename, - $SETTINGS, $post_key ); @@ -287,10 +286,22 @@ $post_backupFile = filter_var($dataReceived['backupFile'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); $post_clearFilename = filter_var($dataReceived['clearFilename'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); $post_offset = (int) filter_var($dataReceived['offset'], FILTER_SANITIZE_NUMBER_INT); - $post_totalSize = (int) filter_var($dataReceived['post_totalSize'], FILTER_SANITIZE_NUMBER_INT); + $post_totalSize = (int) filter_var($dataReceived['totalSize'], FILTER_SANITIZE_NUMBER_INT); $batchSize = 500; - if (WIP === true) error_log('DEBUG: Offset -> '.$post_offset.' | File -> '.$post_clearFilename.' | key -> '.$post_key); + // Check if the offset is greater than the total size + if (empty($post_offset) === false && $post_offset >= $post_totalSize) { + echo prepareExchangedData( + array( + 'error' => false, + 'message' => 'operation_finished', + ), + 'encode' + ); + break; + } + + if (WIP === true) error_log('DEBUG: Offset -> '.$post_offset.'/'.$post_totalSize.' | File -> '.$post_clearFilename.' | key -> '.$post_key); include_once $SETTINGS['cpassman_dir'] . '/sources/main.functions.php'; @@ -311,19 +322,19 @@ ); $post_backupFile = $data['valeur']; - - // Uncrypt the file + + // Decrypt the file if (empty($post_key) === false) { // Decrypt the file + $ret = prepareFileWithDefuse( 'decrypt', $SETTINGS['path_to_files_folder'] . '/' . $post_backupFile, $SETTINGS['path_to_files_folder'] . '/defuse_temp_' . $post_backupFile, - $SETTINGS, $post_key ); - - if (empty($ret) === false) { + + if (empty($ret) === false && $ret !== true) { echo prepareExchangedData( array( 'error' => true, @@ -338,11 +349,19 @@ fileDelete($SETTINGS['path_to_files_folder'] . '/' . $post_backupFile, $SETTINGS); $post_backupFile = $SETTINGS['path_to_files_folder'] . '/defuse_temp_' . $post_backupFile; } else { - $post_backupFile = $SETTINGS['path_to_files_folder'] . '/' . $post_backupFile; + echo prepareExchangedData( + array( + 'error' => true, + 'message' => 'An error occurred. No encryption key provided.', + ), + 'encode' + ); + break; } } else { $post_backupFile = $post_clearFilename; } + //read sql file $handle = fopen($post_backupFile, 'r'); @@ -353,20 +372,19 @@ } if ($handle !== false) { - // Déplacer le pointeur de fichier à l'offset actuel + // Move the file pointer to the current offset fseek($handle, $post_offset); - $query = ''; $executedQueries = 0; while (!feof($handle) && $executedQueries < $batchSize) { $line = fgets($handle); // Check if not false if ($line !== false) { - // Vérifier si la ligne est une partie d'une instruction SQL + // Check if the line is part of an SQL statement if (substr(trim($line), -1) != ';') { $query .= $line; } else { - // Exécuter l'instruction SQL complète + // Execute the complete SQL statement $query .= $line; DB::queryRaw($query); $query = ''; @@ -375,14 +393,14 @@ } } - // Calculer le nouvel offset + // Calculate the new offset $newOffset = ftell($handle); - // Vérifier si la fin du fichier a été atteinte + // Check if the end of the file has been reached $isEndOfFile = feof($handle); fclose($handle); - // Répondre avec le nouvel offset + // Respond with the new offset echo prepareExchangedData( array( 'error' => false, @@ -393,12 +411,13 @@ 'encode' ); - // Vérifier si la fin du fichier a été atteinte pour supprimer le fichier + // Check if the end of the file has been reached to delete the file if ($isEndOfFile) { + error_log('DEBUG: End of file reached. Deleting file '.$post_backupFile); unlink($post_backupFile); } } else { - // Gérer l'erreur d'ouverture du fichier + // Handle file opening error echo prepareExchangedData( array( 'error' => true, @@ -410,3 +429,4 @@ break; } } + diff --git a/sources/identify.php b/sources/identify.php index e8a625db3..01752648a 100755 --- a/sources/identify.php +++ b/sources/identify.php @@ -339,8 +339,8 @@ function identifyUser(string $sentData, array $SETTINGS): bool ], 1 ) === true) - && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1) || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1)) - && $userInfo['mfa_auth_requested_roles'] === true + && (((int) $userInfo['admin'] !== 1 && (int) $userInfo['mfa_enabled'] === 1 && $userInfo['mfa_auth_requested_roles'] === true) + || ((int) $SETTINGS['admin_2fa_required'] === 1 && (int) $userInfo['admin'] === 1)) ) { // Check user against MFA method if selected $userMfa = identifyDoMFAChecks( @@ -2229,6 +2229,9 @@ function identifyDoLDAPChecks( int $sessionPwdAttempts ): array { + $session = SessionManager::getSession(); + $lang = new Language($session->get('user-language') ?? 'english'); + // Prepare LDAP connection if set up if ((int) $SETTINGS['ldap_mode'] === 1 && $username !== 'admin' @@ -2249,7 +2252,7 @@ function identifyDoLDAPChecks( 'initial_url' => isset($sessionUrl) === true ? $sessionUrl : '', 'pwd_attempts' => (int) $sessionPwdAttempts, 'error' => true, - 'message' => "LDAP error: ".$retLDAP['message'], + 'message' => $lang->get('error_bad_credentials'), ] ]; } @@ -2338,7 +2341,7 @@ function shouldUserAuthWithOauth2( // Case where user exists in Teampass but not allowed to auth with Oauth2 return [ 'error' => true, - 'message' => 'user_exists_but_not_oauth2', + 'message' => 'error_bad_credentials', 'oauth2Connection' => false, 'userPasswordVerified' => false, ]; @@ -2617,7 +2620,7 @@ function identifyDoAzureChecks( * @param string $source - The source of the failed attempt (login or remote_ip). * @param string $value - The value for this source (username or IP address). * @param int $limit - The failure attempt limit after which the account/IP - * will be locked. + * will be locked. */ function handleFailedAttempts($source, $value, $limit) { // Count failed attempts from this source @@ -2633,10 +2636,15 @@ function handleFailedAttempts($source, $value, $limit) { $count++; // Calculate unlock time if number of attempts exceeds limit - $unlock_at = $count >= $limit + $unlock_at = $count >= $limit ? date('Y-m-d H:i:s', time() + (($count - $limit + 1) * 600)) : NULL; + // Unlock account one time code + $unlock_code = ($count >= $limit && $source === 'login') + ? generateQuickPassword(30, false) + : NULL; + // Insert the new failure into the database DB::insert( prefixTable('auth_failures'), @@ -2644,8 +2652,41 @@ function handleFailedAttempts($source, $value, $limit) { 'source' => $source, 'value' => $value, 'unlock_at' => $unlock_at, + 'unlock_code' => $unlock_code, ] ); + + if ($unlock_at !== null && $source === 'login') { + $configManager = new ConfigManager(); + $SETTINGS = $configManager->getAllSettings(); + $lang = new Language($SETTINGS['default_language']); + + // Get user email + $userInfos = DB::QueryFirstRow( + 'SELECT email, name + FROM '.prefixTable('users').' + WHERE login = %s', + $value + ); + + // No valid email address for user + if (!$userInfos || !filter_var($userInfos['email'], FILTER_VALIDATE_EMAIL)) + return; + + $unlock_url = $SETTINGS['cpassman_url'].'/self-unlock.php?login='.$value.'&otp='.$unlock_code; + + sendMailToUser( + $userInfos['email'], + $lang->get('bruteforce_reset_mail_body'), + $lang->get('bruteforce_reset_mail_subject'), + [ + '#name#' => $userInfos['name'], + '#reset_url#' => $unlock_url, + '#unlock_at#' => $unlock_at, + ], + true + ); + } } /** @@ -2659,7 +2700,7 @@ function handleFailedAttempts($source, $value, $limit) { */ function addFailedAuthentication($username, $ip) { $user_limit = 10; - $ip_limit = 20; + $ip_limit = 30; // Remove old logs (more than 24 hours) DB::delete( diff --git a/sources/items.queries.php b/sources/items.queries.php index 6a7891bf3..f01ee8c01 100755 --- a/sources/items.queries.php +++ b/sources/items.queries.php @@ -152,6 +152,7 @@ 'notifyType' => $request->request->filter('notify_type', '', FILTER_SANITIZE_SPECIAL_CHARS), 'timestamp' => $request->request->filter('timestamp', '', FILTER_SANITIZE_SPECIAL_CHARS), 'itemKey' => $request->request->filter('item_key', '', FILTER_SANITIZE_SPECIAL_CHARS), + 'action' => $request->request->filter('action', '', FILTER_SANITIZE_SPECIAL_CHARS), ]; $filters = [ @@ -176,6 +177,7 @@ 'notifyType' => 'trim|escape', 'timestamp' => 'cast:integer', 'itemKey' => 'trim|escape', + 'action' => 'trim|escape', ]; $inputData = dataSanitizer( @@ -2836,7 +2838,7 @@ } $arrData['label'] = $dataItem['label'] === '' ? '' : $dataItem['label']; - $arrData['pw'] = $pw; + $arrData['pw_length'] = strlen($pw); $arrData['pw_decrypt_info'] = empty($pw) === true && $pwIsEmptyNormal === false ? 'error_no_sharekey_yet' : ''; $arrData['email'] = empty($dataItem['email']) === true || $dataItem['email'] === null ? '' : $dataItem['email']; $arrData['url'] = empty($dataItem['url']) === true ? '' : $dataItem['url']; @@ -4438,7 +4440,7 @@ break; - case 'show_item_password': + case 'get_item_password': // Check KEY if ($inputData['key'] !== $session->get('key')) { echo (string) prepareExchangedData( @@ -4451,21 +4453,74 @@ break; } - // Run query + // Get item details and its sharekey $dataItem = DB::queryfirstrow( - 'SELECT i.pw AS pw, s.share_key AS share_key + 'SELECT i.pw AS pw, s.share_key AS share_key, i.id AS id, + i.label AS label, i.id_tree AS id_tree FROM ' . prefixTable('items') . ' AS i INNER JOIN ' . prefixTable('sharekeys_items') . ' AS s ON (s.object_id = i.id) - WHERE user_id = %i AND i.item_key = %s', + WHERE user_id = %i AND (i.item_key = %s OR i.id = %i)', $session->get('user-id'), - $inputData['itemKey'] + $inputData['itemKey'] ?? '', + $inputData['itemId'] ?? 0 ); - - // Uncrypt PW + + // Check user access rights if (DB::count() === 0) { - // No share key found - $pw = ''; - } else { + echo (string) prepareExchangedData( + [ + 'error' => true, + 'password' => '', + 'password_error' => $lang->get('password_is_empty'), + ], + 'encode' + ); + break; + } + + // Get user access rights + $userAccess = getCurrentAccessRights( + (int) $session->get('user-id'), + (int) $dataItem['id'], + (int) $dataItem['id_tree'] + )['access']; + + // List of allowed actions + $allowedActions = [ + 'at_password_copied', + 'at_password_shown', + 'at_password_shown_edit_form', + ]; + + // User not allowed to see this password or invalid action provided + if ($userAccess !== true + || empty($inputData['action']) + || !in_array($inputData['action'], $allowedActions, true)) { + + echo (string) prepareExchangedData( + [ + 'error' => true, + 'password' => '', + 'password_error' => $lang->get('not_allowed_to_see_pw'), + ], + 'encode' + ); + break; + } + + // Log the action on password + logItems( + $SETTINGS, + (int) $dataItem['id'], + $dataItem['label'], + (int) $session->get('user-id'), + $inputData['action'], // Filtered by array of allowed values + $session->get('user-login') + ); + + // Uncrypt PW if sharekey is available (empty password otherwise) + $pw = ''; + if (!empty($dataItem['share_key'])) { $pw = doDataDecryption( $dataItem['pw'], decryptUserObjectKey( @@ -4473,14 +4528,6 @@ $session->get('user-private_key') ) ); - - $log = 'Used user ID: '.$session->get('user-id')."\n"; - $log .= 'Used user Private key: '.$session->get('user-private_key')."\n"; - $log .= '$currentUserKey: '.$dataItem['share_key']."\n"; - $log .= 'itemKey: '.decryptUserObjectKey( - $dataItem['share_key'], - $session->get('user-private_key') - )."\n\n"; } $returnValues = array( @@ -4788,15 +4835,21 @@ } elseif ($folder_is_personal === 1) { // Check if personal folder is owned by user - $folder_title = DB::queryFirstRow( - 'SELECT title + $folder = DB::queryFirstRow( + 'SELECT id FROM ' . prefixTable('nested_tree') . ' - WHERE id = %s AND title = %s', - $inputData['folderId'], + WHERE title = %s', $session->get('user-id'), ); - if ($folder_title) $accessLevel = 30; + if ($folder) { + // Get all subfolders of user personal folder + $ids = $tree->getDescendants($folder['id'], true, false, true); + + // This folder is owned by user + if (in_array($inputData['folderId'], $ids)) + $accessLevel = 30; + } } // Access is not allowed to this folder @@ -7309,4 +7362,3 @@ function getAccessResponse(bool $error, bool $access, bool $edit, bool $delete, 'edition_locked' => $editionLocked, ]; } - diff --git a/sources/logs.datatables.php b/sources/logs.datatables.php index 727cb3931..d73052e0c 100755 --- a/sources/logs.datatables.php +++ b/sources/logs.datatables.php @@ -148,7 +148,7 @@ 'SELECT COUNT(*) FROM '.prefixTable('log_system').' as l INNER JOIN '.prefixTable('users').' as u ON (l.qui=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -221,7 +221,7 @@ FROM '.prefixTable('log_items').' as l INNER JOIN '.prefixTable('items').' as i ON (l.id_item=i.id) INNER JOIN '.prefixTable('users').' as u ON (l.id_user=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -294,7 +294,7 @@ FROM '.prefixTable('log_items').' as l INNER JOIN '.prefixTable('items').' as i ON (l.id_item=i.id) INNER JOIN '.prefixTable('users').' as u ON (l.id_user=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -368,7 +368,7 @@ 'SELECT COUNT(*) FROM '.prefixTable('log_system').' as l INNER JOIN '.prefixTable('users').' as u ON (l.qui=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -468,7 +468,7 @@ INNER JOIN '.prefixTable('items').' AS i ON (l.id_item=i.id) INNER JOIN '.prefixTable('users').' AS u ON (l.id_user=u.id) INNER JOIN '.prefixTable('nested_tree').' AS t ON (i.id_tree=t.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -551,7 +551,7 @@ $iTotal = DB::queryFirstField( 'SELECT COUNT(*) FROM '.prefixTable('log_system').' as l - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -627,7 +627,7 @@ 'SELECT COUNT(*) FROM '.prefixTable('log_system').' as l INNER JOIN '.prefixTable('users').' as u ON (l.qui=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -697,7 +697,7 @@ FROM '.prefixTable('items_edition').' AS e INNER JOIN '.prefixTable('items').' as i ON (e.item_id=i.id) INNER JOIN '.prefixTable('users').' as u ON (e.user_id=u.id) - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -773,7 +773,7 @@ $iTotal = DB::queryFirstField( 'SELECT COUNT(*) FROM '.prefixTable('users').' - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -855,7 +855,7 @@ 'SELECT COUNT(*) FROM '.prefixTable('background_tasks').' AS p LEFT JOIN '.prefixTable('users').' AS u ON %l - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', 'u.id = json_extract(p.arguments, "$[0]")', $sWhere, $orderColumn, @@ -863,10 +863,11 @@ ); // Prepare the SQL query - $sql = 'SELECT p.increment_id, p.created_at, p.updated_at, p.process_type, p.is_in_progress - FROM '.prefixTable('background_tasks').' AS p - LEFT JOIN '.prefixTable('users').' AS u ON %l - WHERE %l ORDER BY %s %s LIMIT %i, %i'; + $sql = 'SELECT p.increment_id, p.created_at, p.updated_at, p.process_type, + p.is_in_progress, p.arguments + FROM '.prefixTable('background_tasks').' AS p + LEFT JOIN '.prefixTable('users').' AS u ON %l + WHERE %l ORDER BY %l %l LIMIT %i, %i'; $params = ['u.id = json_extract(p.arguments, "$[0]")',$sWhere, $orderColumn, $orderDirection, $sLimitStart, $sLimitLength]; // Get the records @@ -945,7 +946,7 @@ 'SELECT COUNT(*) FROM '.prefixTable('background_tasks').' AS p LEFT JOIN '.prefixTable('users').' AS u ON u.id = json_extract(p.arguments, "$[0]") - WHERE %l ORDER BY %s %s', + WHERE %l ORDER BY %l %l', $sWhere, $orderColumn, $orderDirection @@ -955,7 +956,7 @@ $sql = 'SELECT p.* FROM '.prefixTable('background_tasks').' AS p LEFT JOIN '.prefixTable('users').' AS u ON %l - WHERE %l ORDER BY %s %s LIMIT %i, %i'; + WHERE %l ORDER BY %l %l LIMIT %i, %i'; $params = ['u.id = json_extract(p.arguments, "$[0]")',$sWhere, $orderColumn, $orderDirection, $sLimitStart, $sLimitLength]; // Get the records diff --git a/sources/main.functions.php b/sources/main.functions.php index 4665b89f1..72356c21a 100755 --- a/sources/main.functions.php +++ b/sources/main.functions.php @@ -134,15 +134,20 @@ function cryption(string $message, string $ascii_key, string $type, ?array $SETT $text = Crypto::decrypt($message, $key); } } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) { - $err = 'an attack! either the wrong key was loaded, or the ciphertext has changed since it was created either corrupted in the database or intentionally modified by someone trying to carry out an attack.'; + error_log('TEAMPASS-Error-Wrong key or modified ciphertext: ' . $ex->getMessage()); + $err = 'wrong_key_or_modified_ciphertext'; } catch (CryptoException\BadFormatException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-Bad format exception: ' . $ex->getMessage()); + $err = 'bad_format'; } catch (CryptoException\EnvironmentIsBrokenException $ex) { - $err = $ex; - } catch (CryptoException\CryptoException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage()); + $err = 'environment_error'; } catch (CryptoException\IOException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-IO: ' . $ex->getMessage()); + $err = 'io_error'; + } catch (Exception $ex) { + error_log('TEAMPASS-Error-Unexpected exception: ' . $ex->getMessage()); + $err = 'unexpected_error'; } return [ @@ -1865,7 +1870,6 @@ function prepareFileWithDefuse( string $type, string $source_file, string $target_file, - array $SETTINGS, string $password = null ) { // Load AntiXSS @@ -1890,7 +1894,6 @@ function prepareFileWithDefuse( $err = defuseFileDecrypt( $source_file, $target_file, - $SETTINGS, /** @scrutinizer ignore-type */ $password ); } elseif ($type === 'encrypt') { @@ -1898,7 +1901,6 @@ function prepareFileWithDefuse( $err = defuseFileEncrypt( $source_file, $target_file, - $SETTINGS, /** @scrutinizer ignore-type */ $password ); } @@ -1920,9 +1922,9 @@ function prepareFileWithDefuse( function defuseFileEncrypt( string $source_file, string $target_file, - array $SETTINGS, string $password = null ) { + $err = ''; try { CryptoFile::encryptFileWithPassword( $source_file, @@ -1932,9 +1934,11 @@ function defuseFileEncrypt( } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) { $err = 'wrong_key'; } catch (CryptoException\EnvironmentIsBrokenException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage()); + $err = 'environment_error'; } catch (CryptoException\IOException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-General: ' . $ex->getMessage()); + $err = 'general_error'; } // return error @@ -1954,9 +1958,9 @@ function defuseFileEncrypt( function defuseFileDecrypt( string $source_file, string $target_file, - array $SETTINGS, string $password = null ) { + $err = ''; try { CryptoFile::decryptFileWithPassword( $source_file, @@ -1966,9 +1970,11 @@ function defuseFileDecrypt( } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) { $err = 'wrong_key'; } catch (CryptoException\EnvironmentIsBrokenException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage()); + $err = 'environment_error'; } catch (CryptoException\IOException $ex) { - $err = $ex; + error_log('TEAMPASS-Error-General: ' . $ex->getMessage()); + $err = 'general_error'; } // return error @@ -2359,6 +2365,9 @@ function doDataDecryption(string $data, string $key): string */ function encryptUserObjectKey(string $key, string $publicKey): string { + // Empty password + if (empty($key)) return ''; + // Sanitize $antiXss = new AntiXSS(); $publicKey = $antiXss->xss_clean($publicKey); @@ -2628,12 +2637,12 @@ function storeUsersShareKey( // Create sharekey for each user $user_ids = [OTV_USER_ID, SSH_USER_ID, API_USER_ID]; if ($all_users_except_id !== -1) { - array_push($user_ids, $all_users_except_id . '"'); + array_push($user_ids, (int) $all_users_except_id); } $users = DB::query( 'SELECT id, public_key FROM ' . prefixTable('users') . ' - WHERE id NOT IN (%li) + WHERE id NOT IN %li AND public_key != ""', $user_ids ); @@ -4307,11 +4316,12 @@ function sendMailToUser( global $SETTINGS; $emailSettings = new EmailSettings($SETTINGS); $emailService = new EmailService(); + $antiXss = new AntiXSS(); // Sanitize inputs $post_receipt = filter_var($post_receipt, FILTER_SANITIZE_EMAIL); - $post_subject = htmlspecialchars($post_subject, ENT_QUOTES, 'UTF-8'); - $post_body = htmlspecialchars($post_body, ENT_QUOTES, 'UTF-8'); + $post_subject = $antiXss->xss_clean($post_subject); + $post_body = $antiXss->xss_clean($post_body); if (count($post_replace) > 0) { $post_body = str_replace( diff --git a/sources/main.queries.php b/sources/main.queries.php index 0d5f35d45..de0d20afe 100755 --- a/sources/main.queries.php +++ b/sources/main.queries.php @@ -776,6 +776,31 @@ function keyHandler(string $post_type, /*php8 array|null|string */$dataReceived, * Launch user recovery download */ case 'user_recovery_keys_download'://action_key + // Validate user password on local and LDAP accounts before download + if ($session->get('user-auth_type') !== 'oauth2') { + // Users passwords are html escaped + $userPassword = filter_var($dataReceived['password'], FILTER_SANITIZE_FULL_SPECIAL_CHARS); + + // Get current user hash + $userHash = DB::queryFirstRow( + "SELECT pw FROM " . prefixtable('users') . " WHERE id = %i;", + $session->get('user-id') + )['pw']; + + $passwordManager = new PasswordManager(); + + // Verify provided user password + if (!$passwordManager->verifyPassword($userHash, $userPassword)) { + return prepareExchangedData( + array( + 'error' => true, + 'message' => $lang->get('error_bad_credentials'), + ), + 'encode' + ); + } + } + return handleUserRecoveryKeysDownload( (int) $filtered_user_id, (array) $SETTINGS, @@ -2968,7 +2993,6 @@ function migrateTo3_DoUserPersonalItemsEncryption( 'decrypt', $SETTINGS['path_to_upload_folder'] . '/' . $record2['file'], $SETTINGS['path_to_upload_folder'] . '/' . $record2['file'] . '.delete', - $SETTINGS, $post_user_psk ); diff --git a/sources/upload.attachments.php b/sources/upload.attachments.php index 82c0bb0f7..eca91bba5 100755 --- a/sources/upload.attachments.php +++ b/sources/upload.attachments.php @@ -117,6 +117,25 @@ handleAttachmentError('No user token found.', 110); exit(); } else { + // Check post_max_size + $POST_MAX_SIZE = ini_get('post_max_size'); + $unit = strtoupper(substr(trim($POST_MAX_SIZE), -1)); // Assurez-vous de bien gérer les espaces éventuels + $units = ['G' => 1073741824, 'M' => 1048576, 'K' => 1024]; + $multiplier = $units[$unit] ?? 1; // Vérifie si l'unité est dans le tableau, sinon 1 + $maxSize = (int)$POST_MAX_SIZE * $multiplier; + + // CHeck if the POST is too big + if (!empty($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > $maxSize && $maxSize > 0) { + handleAttachmentError('POST exceeded maximum allowed size.', 111, 413); + } + + // CHeck if file size is too big + if ($post_fileSize > $maxSize && $maxSize > 0) { + handleAttachmentError('File exceeds the maximum allowed size', 120, 413); + die(); + } + error_log('POST_MAX_SIZE: ' . $POST_MAX_SIZE." - CONTENT_LENGTH: ".$_SERVER['CONTENT_LENGTH']." - UNIT: ".$unit." - MAX: ".$maxSize." - MULTIPLIER: ".$multiplier." - FILE_SIZE: ".$post_fileSize); + // delete expired tokens DB::delete(prefixTable('tokens'), 'end_timestamp < %i', time()); @@ -214,14 +233,6 @@ date_default_timezone_set($post_timezone); } -// Check post_max_size -$POST_MAX_SIZE = ini_get('post_max_size'); -$unit = strtoupper(substr($POST_MAX_SIZE, -1)); -$multiplier = ($unit == 'M' ? 1048576 : ($unit == 'K' ? 1024 : ($unit == 'G' ? 1073741824 : 1))); -if ((int) $_SERVER['CONTENT_LENGTH'] > $multiplier * (int) $POST_MAX_SIZE && $POST_MAX_SIZE) { - handleAttachmentError('POST exceeded maximum allowed size.', 111, 413); -} - // Validate the file size (Warning: the largest files supported by this code is 2GB) $file_size = @filesize($_FILES['file']['tmp_name']); if ($file_size === false || (int) $file_size > (int) $max_file_size_in_bytes) { @@ -483,7 +494,14 @@ function handleAttachmentError($message, $code, $http_code = 400) http_response_code($http_code); // json error message - echo '{"jsonrpc" : "2.0", "error" : {"code": ' . htmlentities((string) $code, ENT_QUOTES) . ', "message": "' . htmlentities((string) $message, ENT_QUOTES) . '"}, "id" : "id"}'; + echo json_encode([ + 'jsonrpc' => '2.0', + 'error' => [ + 'code' => $code, + 'message' => $message + ], + 'id' => 'id' + ]); // Force exit to avoid bypass filters. exit; diff --git a/sources/upload.files.php b/sources/upload.files.php index f441761e0..2e35d7b87 100755 --- a/sources/upload.files.php +++ b/sources/upload.files.php @@ -422,6 +422,14 @@ rename("{$filePath}.part", $filePath); } else { // continue uploading other chunks + echo prepareExchangedData( + array( + 'error' => false, + 'chunk' => (int) $chunk + 1, + 'chunks' => $chunks, + ), + 'encode' + ); die(); } diff --git a/sources/users.datatable.php b/sources/users.datatable.php index 9b85b3ce8..a9266395a 100755 --- a/sources/users.datatable.php +++ b/sources/users.datatable.php @@ -221,13 +221,6 @@ // Display Grid if ($showUserFolders === true) { - /* - // Build list of available users - if ((int) $record['admin'] !== 1 && (int) $record['disabled'] !== 1) { - $listAvailableUsers .= ''; - } - */ - // Get list of allowed functions $listAlloFcts = ''; if ((int) $record['admin'] !== 1) { @@ -251,6 +244,16 @@ $record['id'] ); + // Check for existing lock + $unlock_at = DB::queryFirstField( + 'SELECT MAX(unlock_at) + FROM ' . prefixTable('auth_failures') . ' + WHERE unlock_at > %s AND source = %s AND value = %s', + date('Y-m-d H:i:s', time()), + 'login', + $record['login'] + ); + // Get some infos about user $userDisplayInfos = (isset($userDate['date']) ? 'get('creation_date').': '.date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], (int) $userDate['date']).'\">' : '') @@ -265,7 +268,9 @@ ((in_array($record['id'], [OTV_USER_ID, TP_USER_ID, SSH_USER_ID, API_USER_ID]) === false && (int) $record['admin'] !== 1 && ((int) $SETTINGS['duo'] === 1 || (int) $SETTINGS['google_authentication'] === 1)) ? ((int) $record['mfa_enabled'] === 1 ? '' : 'get('mfa_disabled_for_user').'\">') : '' - ); + ) + . + (($unlock_at) ? 'get('bruteforce_unlock_at').$unlock_at.'\">' : ''); if ($request->query->filter('display_warnings', '', FILTER_VALIDATE_BOOLEAN) === true) { $userDisplayInfos .= '
'. ((in_array($record['id'], [OTV_USER_ID, TP_USER_ID, SSH_USER_ID, API_USER_ID]) === false && (int) $record['admin'] !== 1 && is_null($record['keys_recovery_time']) === true) ? diff --git a/sources/users.queries.php b/sources/users.queries.php index 93292c515..69d1a563a 100755 --- a/sources/users.queries.php +++ b/sources/users.queries.php @@ -2712,6 +2712,32 @@ 'encode' ); + break; + + case "reset_antibruteforce": + // Check KEY + if ($post_key !== $session->get('key')) { + echo prepareExchangedData( + array( + 'error' => true, + 'message' => $lang->get('key_is_not_correct'), + ), + 'encode' + ); + break; + } + + // Prepare variables + $login = getFullUserInfos((int) $dataReceived['user_id'])['login']; + + // Delete all logs for this user + DB::delete( + prefixTable('auth_failures'), + 'source = %s AND value = %s', + 'login', + $login + ); + break; } // # NEW LOGIN FOR USER HAS BEEN DEFINED ##