diff --git a/.plugin.rb.bck.swp b/.plugin.rb.bck.swp new file mode 100644 index 0000000..3fa2797 Binary files /dev/null and b/.plugin.rb.bck.swp differ diff --git a/plugin.rb b/plugin.rb index 4a25f66..a821878 100644 --- a/plugin.rb +++ b/plugin.rb @@ -4,7 +4,7 @@ # name: discourse-md5_authentication # about: A plugin to authenticate users with MD5 passwords from legacy systems -# version: 0.14 +# version: 0.16 # authors: saint@federated.computer # url: https://gitea.federated.computer/saint/discourse-md5_authentication.git @@ -13,200 +13,208 @@ # enabled_site_setting :legacymd5password_auth_enabled after_initialize do - # Extend the SessionController class to include our custom authentication logic - class ::SessionController - prepend Module.new { - # Override the create method to add our custom authentication checks - def create - Rails.logger.warn "MD5 -- AA -- start create" - # Ensure required parameters are present - params.require(:login) - params.require(:password) + # Define a module to hold the legacy MD5 authentication logic + module LegacyMd5Authentication - # Validate the length of the password - return invalid_credentials if params[:password].length > User.max_password_length + # Constants + ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - # Find the user by username or email - user = User.find_by_username_or_email(normalized_login_param) + # Override the create method to add our custom authentication checks + def create + Rails.logger.warn "MD5 -- AA -- start create" + # Ensure required parameters are present + params.require(:login) + params.require(:password) - Rails.logger.warn "MD5 -- BB -- second" - # Check if site is in staff writes-only mode and ensure user is staff if true - raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff? + # Validate the length of the password + return invalid_credentials if params[:password].length > User.max_password_length - # Apply rate limiting for second factor authentication - rate_limit_second_factor!(user) + # Find the user by username or email + user = User.find_by_username_or_email(normalized_login_param) - if user.present? - Rails.logger.warn "MD5 -- CC -- user.present" - # Retrieve the provided password and custom MD5 password hash from user custom fields - password = params[:password] - custom_password_md5 = user.custom_fields['custom_password_md5'] + Rails.logger.warn "MD5 -- BB -- second" + # Check if site is in staff writes-only mode and ensure user is staff if true + raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff? - # Log the presence of custom MD5 hash for debugging - Rails.logger.warn "MD5 -- Check for MD5 password in custom field" - if custom_password_md5.present? - Rails.logger.warn "MD5 -- MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}" + # Apply rate limiting for second factor authentication + rate_limit_second_factor!(user) - # Verify the provided password against the stored MD5 hash - if verify_gossamer_password(password, custom_password_md5) - # If MD5 hash matches, update the user's password and other attributes - Rails.logger.warn "MD5 -- MD5 matches" + if user.present? + Rails.logger.warn "MD5 -- CC -- user.present" + # Retrieve the provided password and custom MD5 password hash from user custom fields + password = params[:password] + custom_password_md5 = user.custom_fields['custom_password_md5'] - # Set the user's password to the provided one and update other attributes - user.password = password - user.active = true - user.approved = true - user.approved_at = Time.now - user.approved_by_id = 1 - user.custom_fields['custom_password_md5'] = nil # Clear the custom MD5 field - user.save! + # Log the presence of custom MD5 hash for debugging + Rails.logger.warn "MD5 -- Check for MD5 password in custom field" + if custom_password_md5.present? + Rails.logger.warn "MD5 -- MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}" + + # Verify the provided password against the stored MD5 hash + if verify_gossamer_password(password, custom_password_md5) + # If MD5 hash matches, update the user's password and other attributes + Rails.logger.warn "MD5 -- MD5 matches" + + # Set the user's password to the provided one and update other attributes + user.password = password + user.active = true + user.approved = true + user.approved_at = Time.now + user.approved_by_id = 1 + user.custom_fields['custom_password_md5'] = nil # Clear the custom MD5 field + user.save! Rails.logger.warn "MD5 -- DD -- user.present" - # Generate a new token and hash it - token = SecureRandom.hex(20) - token_hash = EmailToken.hash_token(token) + # Generate a new token and hash it + token = SecureRandom.hex(20) + token_hash = EmailToken.hash_token(token) - # Create a confirmed email token for the user - EmailToken.create!( - user_id: user.id, - email: user.email, - token_hash: token_hash, - confirmed: true - ) - Rails.logger.warn("MD5 -- Generated token for user #{user.username}: #{token}") + # Create a confirmed email token for the user + EmailToken.create!( + user_id: user.id, + email: user.email, + token_hash: token_hash, + confirmed: true + ) + Rails.logger.warn("MD5 -- Generated token for user #{user.username}: #{token}") - Rails.logger.warn "MD5 -- Updated user: #{user.id}" - else - # If MD5 hash does not match, log the failed login attempt - Rails.logger.warn "MD5 -- MD5 Password (hash) incorrect for user: #{user.id}" - invalid_credentials - return - end - - elsif !user.confirm_password?(password) - # If no MD5 hash is present and the provided password is incorrect - Rails.logger.warn "MD5 -- Password incorrect for user: #{user.id}" + Rails.logger.warn "MD5 -- Updated user: #{user.id}" + else + # If MD5 hash does not match, log the failed login attempt + Rails.logger.warn "MD5 -- MD5 Password (hash) incorrect for user: #{user.id}" invalid_credentials return end - # If the site requires user approval and the user is not yet approved - if login_not_approved_for?(user) - render json: login_not_approved - return - end - - # Invalidate any invite link for the user - Invite.invalidate_for_email(user.email) - - # Check if the user's password has expired - if user.password_expired?(password) - render json: { error: "expired", reason: "expired" } - return - end - else - # If no user is found with the provided credentials + elsif !user.confirm_password?(password) + # If no MD5 hash is present and the provided password is incorrect + Rails.logger.warn "MD5 -- Password incorrect for user: #{user.id}" invalid_credentials return end - # Check for any login errors - if payload = login_error_check(user) - return render json: payload + # If the site requires user approval and the user is not yet approved + if login_not_approved_for?(user) + render json: login_not_approved + return end - # Authenticate second factor if required - second_factor_auth_result = authenticate_second_factor(user) - return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok + # Invalidate any invite link for the user + Invite.invalidate_for_email(user.email) - # If user is active and email is confirmed, proceed with login - if user.active && user.email_confirmed? - login(user, second_factor_auth_result) - else - not_activated(user) + # Check if the user's password has expired + if user.password_expired?(password) + render json: { error: "expired", reason: "expired" } + return end + else + # If no user is found with the provided credentials + invalid_credentials + return end - } - end -end -# Helper methods to handle MD5 password verification + # Check for any login errors + if payload = login_error_check(user) + return render json: payload + end -def to64(value, length) - # Convert a value to a base64-like representation - result = String.new - length.times do - result << ITOA64[value & 0x3f] - value >>= 6 - Rails.logger.warn "to64 result: #{result}" - end - result -end + # Authenticate second factor if required + second_factor_auth_result = authenticate_second_factor(user) + return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok -def gossamer_md5_crypt(password, legacy_hash) - # Extract the salt from the legacy hash - parts = legacy_hash.split('$') - salt = parts[2] - - # Limit the salt to 8 characters - salt = salt[0, 8] - - magic = "$GT$" - Rails.logger.warn "MD5 magic: #{magic}" - - ctx = Digest::MD5.new - ctx.update(password) - ctx.update(magic) - ctx.update(salt) - - final = Digest::MD5.new - final.update(password) - final.update(salt) - final.update(password) - final_digest = final.digest - - password_length = password.length - - # Process the password with various hashing operations - while password_length > 0 - ctx.update(final_digest[0, [password_length, 16].min]) - password_length -= 16 - end - - password_length = password.length - while password_length > 0 - if password_length & 1 != 0 - ctx.update("\x00") - else - ctx.update(password[0, 1]) + # If user is active and email is confirmed, proceed with login + if user.active && user.email_confirmed? + login(user, second_factor_auth_result) + else + not_activated(user) + end + end + + # Helper methods to handle MD5 password verification + + def to64(value, length) + # Convert a value to a base64-like representation + result = String.new + length.times do + result << ITOA64[value & 0x3f] + value >>= 6 + Rails.logger.warn "to64 result: #{result}" + end + result + end + + def gossamer_md5_crypt(password, legacy_hash) + # Extract the salt from the legacy hash + parts = legacy_hash.split('$') + salt = parts[2] + + # Limit the salt to 8 characters + salt = salt[0, 8] + + magic = "$GT$" + Rails.logger.warn "MD5 magic: #{magic}" + + ctx = Digest::MD5.new + ctx.update(password) + ctx.update(magic) + ctx.update(salt) + + final = Digest::MD5.new + final.update(password) + final.update(salt) + final.update(password) + final_digest = final.digest + + password_length = password.length + + # Process the password with various hashing operations + while password_length > 0 + ctx.update(final_digest[0, [password_length, 16].min]) + password_length -= 16 + end + + password_length = password.length + while password_length > 0 + if password_length & 1 != 0 + ctx.update("\x00") + else + ctx.update(password[0, 1]) + end + password_length >>= 1 + end + + final_digest = ctx.digest + Rails.logger.warn "MD6 final_digest: #{final_digest}" + + result = String.new + Rails.logger.warn "A result: #{result}" + result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4) + Rails.logger.warn "B result: #{result}" + result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4) + Rails.logger.warn "C result: #{result}" + result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4) + Rails.logger.warn "D result: #{result}" + result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4) + Rails.logger.warn "E result: #{result}" + result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4) + Rails.logger.warn "F result: #{result}" + result << to64(final_digest[11].ord, 2) + Rails.logger.warn "G result: #{result}" + + Rails.logger.warn "magic salt result #{magic}#{salt}$#{result}" + "#{magic}#{salt}$#{result}" + end + + def verify_gossamer_password(password, legacy_hash) + # Verify the provided password against the stored MD5 hash + generated_hash = gossamer_md5_crypt(password, legacy_hash) + generated_hash == legacy_hash end - password_length >>= 1 end - final_digest = ctx.digest - Rails.logger.warn "MD6 final_digest: #{final_digest}" - - result = String.new - Rails.logger.warn "A result: #{result}" - result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4) - Rails.logger.warn "B result: #{result}" - result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4) - Rails.logger.warn "C result: #{result}" - result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4) - Rails.logger.warn "D result: #{result}" - result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4) - Rails.logger.warn "E result: #{result}" - result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4) - Rails.logger.warn "F result: #{result}" - result << to64(final_digest[11].ord, 2) - Rails.logger.warn "G result: #{result}" - - Rails.logger.warn "magic salt result #{magic}#{salt}$#{result}" - "#{magic}#{salt}$#{result}" + # Extend the SessionController class to include the LegacyMd5Authentication module + class ::SessionController + prepend LegacyMd5Authentication + end end -def verify_gossamer_password(password, legacy_hash) - # Verify the provided password against the stored MD5 hash - generated_hash = gossamer_md5_crypt(password, legacy_hash) - generated_hash == legacy_hash -end