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
This commit is contained in:
Tom Price 2018-03-14 16:29:26 +00:00
parent 116ac139cf
commit e641ae70bd
6 changed files with 283 additions and 27 deletions

View file

@ -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;

View file

@ -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

View file

@ -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`""")

View file

@ -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))

View file

@ -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""")

View file

@ -48,6 +48,7 @@ googlemaps
mycli
braintree
future
passlib
google-api-python-client
google-auth
google-auth-httplib2