From e641ae70bde01cd90ed0656256c84dd5ca193558 Mon Sep 17 00:00:00 2001 From: Tom Price Date: Wed, 14 Mar 2018 16:29:26 +0000 Subject: [PATCH] Migrate password hashing away from mysql password() This is deprecated and needs to be replaced. Use passlib to hash, store, verify and upgrade as necessary. Includes patch to migrate existing passwords in a non-breaking way. Fixes #5195 --- frappe/data/Framework.sql | 1 - frappe/patches.txt | 210 +++++++++++++++++- .../v10_0/migrate_passwords_passlib.py | 21 ++ frappe/tests/test_password.py | 10 +- frappe/utils/password.py | 67 ++++-- requirements.txt | 1 + 6 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 frappe/patches/v10_0/migrate_passwords_passlib.py diff --git a/frappe/data/Framework.sql b/frappe/data/Framework.sql index 4e97b0bfca..071145b6f5 100644 --- a/frappe/data/Framework.sql +++ b/frappe/data/Framework.sql @@ -215,7 +215,6 @@ CREATE TABLE `__Auth` ( `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, `password` VARCHAR(255) NOT NULL, - `salt` VARCHAR(140), `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/patches.txt b/frappe/patches.txt index 9d355b35d2..954c233666 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -1,4 +1,4 @@ -execute:frappe.db.sql("""update `tabPatch Log` set patch=replace(patch, '.4_0.', '.v4_0.')""") #2014-05-12 + frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4 execute:frappe.utils.global_search.setup_global_search_table() frappe.patches.v8_0.update_global_search_table @@ -205,4 +205,212 @@ frappe.patches.v10_0.refactor_social_login_keys frappe.patches.v10_0.enable_chat_by_default_within_system_settings frappe.patches.v10_0.remove_custom_field_for_disabled_domain execute:frappe.delete_doc("Page", "chat") + +frappe.patches.v5_0.convert_to_barracuda_and_utf8mb4 +execute:frappe.utils.global_search.setup_global_search_table() +frappe.patches.v8_0.update_global_search_table +frappe.patches.v7_0.update_auth +frappe.patches.v8_0.drop_in_dialog #2017-09-22 +frappe.patches.v7_2.remove_in_filter +execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 +execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 +execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 +execute:frappe.reload_doc('core', 'doctype', 'module_def') #2017-09-22 +execute:frappe.reload_doc('core', 'doctype', 'version') #2017-04-01 +execute:frappe.reload_doc('core', 'doctype', 'activity_log') +frappe.patches.v7_1.rename_scheduler_log_to_error_log +frappe.patches.v6_1.rename_file_data +frappe.patches.v7_0.re_route #2016-06-27 +frappe.patches.v8_0.drop_is_custom_from_docperm +frappe.patches.v8_0.update_records_in_global_search #11-05-2017 +frappe.patches.v8_0.update_published_in_global_search +execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') +execute:frappe.reload_doc('core', 'doctype', 'deleted_document') +execute:frappe.reload_doc('core', 'doctype', 'domain_settings') +frappe.patches.v8_0.rename_page_role_to_has_role #2017-03-16 +frappe.patches.v7_2.setup_custom_perms #2017-01-19 +frappe.patches.v8_0.set_user_permission_for_page_and_report #2017-03-20 +execute:frappe.reload_doc('core', 'doctype', 'role') #2017-05-23 +execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27 +execute:frappe.reload_doc('custom', 'doctype', 'custom_field') #2015-10-19 +execute:frappe.reload_doc('core', 'doctype', 'page') #2013-13-26 +execute:frappe.reload_doc('core', 'doctype', 'report') #2014-06-03 +execute:frappe.reload_doc('core', 'doctype', 'translation') #2016-03-03 +execute:frappe.reload_doc('email', 'doctype', 'email_alert') #2014-07-15 +execute:frappe.reload_doc('desk', 'doctype', 'todo') #2014-12-31-1 +execute:frappe.reload_doc('custom', 'doctype', 'property_setter') #2014-12-31-1 +execute:frappe.reload_doc('core', 'doctype', 'patch_log') #2016-10-31 +execute:frappe.reload_doctype("File") # 2015-10-19 +execute:frappe.reload_doc('core', 'doctype', 'error_snapshot') +execute:frappe.clear_cache() +frappe.patches.v7_1.rename_scheduler_log_to_error_log +frappe.patches.v7_1.sync_language_doctype +frappe.patches.v7_0.rename_bulk_email_to_email_queue +frappe.patches.v7_1.rename_chinese_language_codes + +execute:frappe.db.sql("alter table `tabSessions` modify `user` varchar(255), engine=InnoDB") +execute:frappe.db.sql("delete from `tabDocField` where parent='0'") +frappe.patches.v4_0.change_varchar_length +frappe.patches.v6_4.reduce_varchar_length +frappe.patches.v5_2.change_checks_to_not_null +frappe.patches.v6_9.int_float_not_null #2015-11-25 +frappe.patches.v5_0.v4_to_v5 + +frappe.patches.v5_0.remove_shopping_cart_app +frappe.patches.v4_0.webnotes_to_frappe +execute:frappe.permissions.reset_perms("Module Def") +execute:import frappe.installer;frappe.installer.make_site_dirs() #2014-02-19 +frappe.patches.v4_0.rename_profile_to_user +frappe.patches.v4_0.deprecate_control_panel +frappe.patches.v4_0.remove_old_parent +frappe.patches.v4_0.rename_sitemap_to_route +frappe.patches.v4_0.website_sitemap_hierarchy +frappe.patches.v4_0.remove_index_sitemap +frappe.patches.v4_0.set_website_route_idx +frappe.patches.v4_0.add_delete_permission +frappe.patches.v4_0.set_todo_checked_as_closed +frappe.patches.v4_0.private_backups +frappe.patches.v4_0.set_module_in_report +frappe.patches.v4_0.update_datetime +frappe.patches.v4_0.file_manager_hooks +execute:frappe.get_doc("User", "Guest").save() +frappe.patches.v4_0.update_custom_field_insert_after +frappe.patches.v4_0.deprecate_link_selects +frappe.patches.v4_0.set_user_gravatar +frappe.patches.v4_0.set_user_permissions +frappe.patches.v4_0.create_custom_field_for_owner_match +frappe.patches.v4_0.enable_scheduler_in_system_settings +execute:frappe.db.sql("update tabReport set apply_user_permissions=1") #2014-06-03 +frappe.patches.v4_0.replace_deprecated_timezones +execute:import frappe.website.render; frappe.website.render.clear_cache("login"); #2014-06-10 +frappe.patches.v4_0.fix_attach_field_file_url +execute:frappe.permissions.reset_perms("User") #2015-03-24 +execute:frappe.db.sql("""delete from `tabUserRole` where ifnull(parentfield, '')='' or ifnull(`role`, '')=''""") #2014-08-18 +frappe.patches.v4_0.remove_user_owner_custom_field +execute:frappe.delete_doc("DocType", "Website Template") +execute:frappe.db.sql("""update `tabProperty Setter` set property_type='Text' where property in ('options', 'default')""") #2014-06-20 +frappe.patches.v4_1.enable_outgoing_email_settings +execute:frappe.db.sql("""update `tabSingles` set `value`=`doctype` where `field`='name'""") #2014-07-04 +frappe.patches.v4_1.enable_print_as_pdf #2014-06-17 +execute:frappe.db.sql("""update `tabDocPerm` set email=1 where parent='User' and permlevel=0 and `role`='All' and `read`=1 and apply_user_permissions=1""") #2014-07-15 +execute:frappe.db.sql("""update `tabPrint Format` set print_format_type='Client' where ifnull(print_format_type, '')=''""") #2014-07-28 +frappe.patches.v4_1.file_manager_fix +frappe.patches.v4_2.print_with_letterhead +execute:frappe.delete_doc("DocType", "Control Panel", force=1) +execute:frappe.reload_doc('website', 'doctype', 'web_form') #2014-09-04 +execute:frappe.reload_doc('website', 'doctype', 'web_form_field') #2014-09-04 +frappe.patches.v4_2.refactor_website_routing +frappe.patches.v4_2.set_assign_in_doc +frappe.patches.v4_3.remove_allow_on_submit_customization +frappe.patches.v5_0.rename_table_fieldnames +frappe.patches.v5_0.communication_parent +frappe.patches.v5_0.clear_website_group_and_notifications +execute:frappe.db.sql("""update tabComment set comment = substr(comment, 6, locate(":", comment)-6) where comment_type in ("Assigned", "Assignment Completed")""") +execute:frappe.db.sql("update `tabComment` set comment_type='Comment' where comment_doctype='Blog Post' and ifnull(comment_type, '')=''") +frappe.patches.v5_0.update_shared +execute:frappe.reload_doc("core", "doctype", "docshare") #2015-07-21 +frappe.patches.v6_19.comment_feed_communication +frappe.patches.v6_16.star_to_like +frappe.patches.v5_0.bookmarks_to_stars +frappe.patches.v5_0.style_settings_to_website_theme +frappe.patches.v5_0.rename_ref_type_fieldnames +frappe.patches.v5_0.fix_email_alert +frappe.patches.v5_0.fix_null_date_datetime +frappe.patches.v5_0.force_sync_website +execute:frappe.delete_doc("DocType", "Tag") +execute:frappe.db.sql("delete from `tabProperty Setter` where `property` in ('idx', '_idx')") +frappe.patches.v5_0.move_scheduler_last_event_to_system_settings +execute:frappe.db.sql("update tabUser set new_password='' where ifnull(new_password, '')!=''") +frappe.patches.v5_0.fix_text_editor_file_urls +frappe.patches.v5_0.modify_session +frappe.patches.v5_0.expire_old_scheduler_logs +execute:frappe.permissions.reset_perms("DocType") +execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx'") +frappe.patches.v6_0.communication_status_and_permission +frappe.patches.v6_0.make_task_log_folder +frappe.patches.v6_0.document_type_rename +frappe.patches.v6_0.fix_ghana_currency +frappe.patches.v6_2.ignore_user_permissions_if_missing +execute:frappe.db.sql("delete from tabSessions where user is null") +frappe.patches.v6_2.rename_backup_manager +execute:frappe.delete_doc("DocType", "Backup Manager") +execute:frappe.db.sql("""update `tabCommunication` set parenttype=null, parent=null, parentfield=null""") #2015-10-22 +execute:frappe.permissions.reset_perms("Web Page") +frappe.patches.v6_6.user_last_active +frappe.patches.v6_6.fix_file_url +frappe.patches.v6_11.rename_field_in_email_account +frappe.patches.v7_0.create_private_file_folder +frappe.patches.v6_15.remove_property_setter_for_previous_field #2015-12-29 +frappe.patches.v6_15.set_username +execute:frappe.permissions.reset_perms("Error Snapshot") +frappe.patches.v6_16.feed_doc_owner +frappe.patches.v6_21.print_settings_repeat_header_footer +frappe.patches.v6_24.set_language_as_code +frappe.patches.v6_20x.update_insert_after +finally:frappe.patches.v6_24.sync_desktop_icons +frappe.patches.v6_20x.set_allow_draft_for_print +frappe.patches.v6_20x.remove_roles_from_website_user +frappe.patches.v7_0.set_user_fullname +frappe.patches.v7_0.desktop_icons_hidden_by_admin_as_blocked +frappe.patches.v7_0.add_communication_in_doc +frappe.patches.v7_0.update_send_after_in_bulk_email +execute:frappe.db.sql('''delete from `tabSingles` where doctype="Email Settings"''') # 2016-06-13 +execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''") +frappe.patches.v7_0.rename_newsletter_list_to_email_group +frappe.patches.v7_0.replace_upgrade_link_limit +frappe.patches.v7_0.set_email_group +frappe.patches.v7_1.setup_integration_services #2016-10-27 +frappe.patches.v7_1.rename_chinese_language_codes +execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12 +execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1) +frappe.patches.v7_0.cleanup_list_settings +execute:frappe.db.set_default('language', '') +frappe.patches.v7_1.refactor_integration_broker +frappe.patches.v7_1.set_backup_limit +frappe.patches.v7_2.set_doctype_engine +frappe.patches.v7_2.merge_knowledge_base +frappe.patches.v7_0.update_report_builder_json +frappe.patches.v7_2.set_in_standard_filter_property #1 +frappe.patches.v8_0.drop_unwanted_indexes +execute:frappe.db.sql("update tabCommunication set communication_date = creation where time(communication_date) = 0") +frappe.patches.v7_2.fix_email_queue_recipient +frappe.patches.v7_2.update_feedback_request # 2017-02-27 +execute:frappe.rename_doc('Country', 'Macedonia, Republic of', 'Macedonia', ignore_if_exists=True) +execute:frappe.rename_doc('Country', 'Iran, Islamic Republic of', 'Iran', ignore_if_exists=True) +execute:frappe.rename_doc('Country', 'Tanzania, United Republic of', 'Tanzania', ignore_if_exists=True) +execute:frappe.rename_doc('Country', 'Syrian Arab Republic', 'Syria', ignore_if_exists=True) +frappe.patches.v8_0.rename_listsettings_to_usersettings +frappe.patches.v7_2.update_communications +frappe.patches.v8_0.deprecate_integration_broker +frappe.patches.v8_0.update_gender_and_salutation +frappe.patches.v8_0.setup_email_inbox #2017-03-29 +frappe.patches.v8_0.newsletter_childtable_migrate +execute:frappe.db.sql("delete from `tabDesktop Icon` where module_name='Communication'") +execute:frappe.db.sql("update `tabDesktop Icon` set type='list' where _doctype='Communication'") +frappe.patches.v8_0.fix_non_english_desktop_icons # 2017-04-12 +frappe.patches.v8_0.set_doctype_values_in_custom_role +frappe.patches.v8_0.install_new_build_system_requirements +frappe.patches.v8_0.set_currency_field_precision # 2017-05-09 +frappe.patches.v8_0.rename_print_to_printing +frappe.patches.v7_1.disabled_print_settings_for_custom_print_format +frappe.patches.v8_0.update_desktop_icons +execute:frappe.db.sql('update tabReport set module="Desk" where name="ToDo"') +frappe.patches.v8_1.enable_allow_error_traceback_in_system_settings +frappe.patches.v8_1.update_format_options_in_auto_email_report +frappe.patches.v8_1.delete_custom_docperm_if_doctype_not_exists +frappe.patches.v8_5.delete_email_group_member_with_invalid_emails +frappe.patches.v8_x.update_user_permission +frappe.patches.v8_5.patch_event_colors +frappe.patches.v8_10.delete_static_web_page_from_global_search +frappe.patches.v9_1.add_sms_sender_name_as_parameters +frappe.patches.v9_1.resave_domain_settings +frappe.patches.v9_1.revert_domain_settings +frappe.patches.v9_1.move_feed_to_activity_log +execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) +frappe.patches.v10_0.reload_countries_and_currencies +frappe.patches.v10_0.refactor_social_login_keys +frappe.patches.v10_0.enable_chat_by_default_within_system_settings +frappe.patches.v10_0.remove_custom_field_for_disabled_domain +execute:frappe.delete_doc("Page", "chat") +frappe.patches.v10_0.migrate_passwords_passlib frappe.patches.v11_0.drop_column_apply_user_permissions diff --git a/frappe/patches/v10_0/migrate_passwords_passlib.py b/frappe/patches/v10_0/migrate_passwords_passlib.py new file mode 100644 index 0000000000..ada50e46f6 --- /dev/null +++ b/frappe/patches/v10_0/migrate_passwords_passlib.py @@ -0,0 +1,21 @@ +import frappe +from frappe.utils.password import LegacyPassword + + +def execute(): + all_auths = frappe.db.sql("""SELECT `name`, `password`, `salt` FROM `__Auth` + WHERE doctype='User' AND `fieldname`='password'""", + as_dict=True + ) + + for auth in all_auths: + if auth.salt and auth.salt != "": + pwd = LegacyPassword.hash(auth.password, salt=auth.salt.encode('UTF-8')) + frappe.db.sql("""UPDATE `__Auth` SET `password`=%(pwd)s, `salt`=NULL + WHERE `doctype`='User' AND `fieldname`='password' AND `name`=%(user)s""", + {'pwd': pwd, 'user': auth.name} + ) + + frappe.reload_doctype("User") + + frappe.db.sql_ddl("""ALTER TABLE `__Auth` DROP COLUMN `salt`""") diff --git a/frappe/tests/test_password.py b/frappe/tests/test_password.py index 0bf9bbfb37..8b429e4270 100644 --- a/frappe/tests/test_password.py +++ b/frappe/tests/test_password.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.utils.password import update_password, check_password +from frappe.utils.password import update_password, check_password, passlibctx class TestPassword(unittest.TestCase): def setUp(self): @@ -52,14 +52,14 @@ class TestPassword(unittest.TestCase): update_password(user, new_password) - auth = frappe.db.sql('''select `password`, `salt` from `__Auth` + auth = frappe.db.sql('''select `password` from `__Auth` where doctype='User' and name=%s and fieldname="password"''', user, as_dict=True)[0] + # is not plain text self.assertTrue(auth.password != new_password) - self.assertTrue(auth.salt) - # stored password = password(plain_text_password + salt) - self.assertEqual(frappe.db.sql('select password(concat(%s, %s))', (new_password, auth.salt))[0][0], auth.password) + # is valid hashing + self.assertTrue(passlibctx.verify(new_password, auth.password)) self.assertTrue(check_password(user, new_password)) diff --git a/frappe/utils/password.py b/frappe/utils/password.py index 182b1fb011..d7dbea06b3 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -2,10 +2,41 @@ # MIT License. See license.txt from __future__ import unicode_literals +import string import frappe from frappe import _ from frappe.utils import cstr, encode from cryptography.fernet import Fernet, InvalidToken +from passlib.hash import pbkdf2_sha256, mysql41 +from passlib.registry import register_crypt_handler +from passlib.context import CryptContext + + +class LegacyPassword(pbkdf2_sha256): + name = "frappe_legacy" + ident = "$frappel$" + + def _calc_checksum(self, secret): + # check if this is a mysql hash + # it is possible that we will generate a false positive if the users password happens to be 40 hex chars proceeded + # by an * char, but this seems highly unlikely + if not (secret[0] == "*" and len(secret) == 41 and all(c in string.hexdigits for c in secret[1:])): + secret = mysql41.hash(secret + self.salt) + return super(LegacyPassword, self)._calc_checksum(secret) + + +register_crypt_handler(LegacyPassword, force=True) +passlibctx = CryptContext( + schemes=[ + "pbkdf2_sha256", + "argon2", + "frappe_legacy", + ], + deprecated=[ + "frappe_legacy", + ], +) + def get_decrypted_password(doctype, name, fieldname='password', raise_exception=True): auth = frappe.db.sql('''select `password` from `__Auth` @@ -24,27 +55,24 @@ def set_encrypted_password(doctype, name, pwd, fieldname='password'): on duplicate key update `password`=%(pwd)s, encrypted=1""", { 'doctype': doctype, 'name': name, 'fieldname': fieldname, 'pwd': encrypt(pwd) }) -def check_password(user, pwd, doctype='User', fieldname='password'): +def check_password(auth, pwd, doctype='User', fieldname='password'): '''Checks if user and password are correct, else raises frappe.AuthenticationError''' - auth = frappe.db.sql("""select name, `password`, salt from `__Auth` - where doctype=%(doctype)s and name=%(name)s and fieldname=%(fieldname)s and encrypted=0 - and ( - (salt is null and `password`=password(%(pwd)s)) - or `password`=password(concat(%(pwd)s, salt)) - )""",{ 'doctype': doctype, 'name': user, 'fieldname': fieldname, 'pwd': pwd }, as_dict=True) + auth = frappe.db.sql("""select name, `password` from `__Auth` + where doctype=%(doctype)s and name=%(name)s and fieldname=%(fieldname)s and encrypted=0""", + {'doctype': doctype, 'name': auth, 'fieldname': fieldname}, + as_dict=True + ) - if not auth: - raise frappe.AuthenticationError('Incorrect User or Password') - - salt = auth[0].salt - if not salt: - # sets salt and updates password - update_password(user, pwd, doctype, fieldname) + if not auth or not passlibctx.verify(pwd, auth[0].password): + raise frappe.AuthenticationError(_('Incorrect User or Password')) # lettercase agnostic user = auth[0].name + if not passlibctx.needs_update(auth[0].password): + update_password(user, pwd, doctype, fieldname) + return user def update_password(user, pwd, doctype='User', fieldname='password', logout_all_sessions=False): @@ -57,12 +85,12 @@ def update_password(user, pwd, doctype='User', fieldname='password', logout_all_ :param fieldname: fieldname (in given doctype) (for encryption) :param logout_all_session: delete all other session ''' - salt = frappe.generate_hash() - frappe.db.sql("""insert into __Auth (doctype, name, fieldname, `password`, salt, encrypted) - values (%(doctype)s, %(name)s, %(fieldname)s, password(concat(%(pwd)s, %(salt)s)), %(salt)s, 0) + hashPwd = passlibctx.hash(pwd) + frappe.db.sql("""insert into __Auth (doctype, name, fieldname, `password`, encrypted) + values (%(doctype)s, %(name)s, %(fieldname)s, %(pwd)s, 0) on duplicate key update - `password`=password(concat(%(pwd)s, %(salt)s)), salt=%(salt)s, encrypted=0""", - { 'doctype': doctype, 'name': user, 'fieldname': fieldname, 'pwd': pwd, 'salt': salt }) + `password`=%(pwd)s, encrypted=0""", + {'doctype': doctype, 'name': user, 'fieldname': fieldname, 'pwd': hashPwd}) # clear all the sessions except current if logout_all_sessions: @@ -95,7 +123,6 @@ def create_auth_table(): `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, `password` VARCHAR(255) NOT NULL, - `salt` VARCHAR(140), `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""") diff --git a/requirements.txt b/requirements.txt index 884acfd59d..ef9203659d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,6 +48,7 @@ googlemaps mycli braintree future +passlib google-api-python-client google-auth google-auth-httplib2