# plugins/discourse-md5_authentication/plugin.rb # frozen_string_literal: true # name: discourse-md5_authentication # about: A plugin to authenticate users with MD5 passwords from legacy systems # version: 0.9.6 # authors: saint # url: https://gitea.federated.computer/saint/discourse-md5_authentication.git # require 'digest' after_initialize do class ::SessionController < ApplicationController # Constants ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" def create # Require the necessary parameters params.require(:login) params.require(:password) # Validate password length return invalid_credentials if params[:password].length > User.max_password_length # Find user by username or email user = User.find_by_username_or_email(normalized_login_param) # Check for read-only mode for non-staff users raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff? # Apply rate limit for second factor authentication rate_limit_second_factor!(user) if user.present? password = params[:password] custom_password_md5 = user.custom_fields['custom_password_md5'] # Check for MD5 password in custom field if custom_password_md5.present? Rails.logger.debug "MD5 password is present. custom_password_md5: #{custom_password_md5}, password: #{password}" # Extract the salt from the legacy hash parts = custom_password_md5.split('$') Rails.logger.debug "Split parts: #{parts.inspect}" if parts.length >= 3 salt = parts[2][0, 8] else Rails.logger.debug "Invalid MD5 format for custom_password_md5: #{custom_password_md5}" return invalid_credentials end magic = "$GT$" Rails.logger.debug "MD5 magic: #{magic}, salt: #{salt}" # Create initial MD5 context ctx = Digest::MD5.new ctx.update(password) ctx.update(magic) ctx.update(salt) # Create final MD5 digest final = Digest::MD5.new final.update(password) final.update(salt) final.update(password) final_digest = final.digest.dup # Ensure final_digest is not frozen # Perform password length operations password_length = password.length 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.dup # Ensure final_digest is not frozen Rails.logger.debug "MD5 final_digest after initial operations: #{final_digest}" # Perform 1000 iterations of MD5 hashing 1000.times do |i| ctx1 = Digest::MD5.new if i & 1 != 0 ctx1.update(password) else ctx1.update(final_digest.dup) # Ensure final_digest is not frozen end ctx1.update(salt) if i % 3 != 0 ctx1.update(password) if i % 7 != 0 if i & 1 != 0 ctx1.update(final_digest.dup) # Ensure final_digest is not frozen else ctx1.update(password) end final_digest = ctx1.digest.dup # Ensure final_digest is not frozen end # Convert final digest to the hashed password format result = '' length = 4 value = (final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end value = (final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end value = (final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end value = (final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end value = (final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end length = 2 value = final_digest[11].ord length.times do result << ITOA64[value & 0x3f] value >>= 6 end entered_password_md5 = "#{magic}#{salt}$#{result}" Rails.logger.debug "Generated MD5 hash: #{entered_password_md5}" # Verify the entered password MD5 hash against the stored hash if entered_password_md5 == custom_password_md5 Rails.logger.debug "MD5 hash matches for user: #{user.id}" # MD5 matches, update the user's password to the new one and remove the custom field user.password = password user.custom_fields['custom_password_md5'] = nil user.save! Rails.logger.debug "Updated MD5 password for user: #{user.id}" else Rails.logger.debug "MD5 password incorrect for user: #{user.id}" return invalid_credentials end elsif !user.confirm_password?(password) Rails.logger.debug "Password incorrect for user: #{user.id}" return invalid_credentials end # Handle user approval requirements if login_not_approved_for?(user) render json: login_not_approved return end # Invalidate invite link if user signed on with username and password Invite.invalidate_for_email(user.email) # Handle password expiration if user.password_expired?(password) render json: { error: "expired", reason: "expired" } return end else Rails.logger.debug "User not found with login: #{params[:login]}" return invalid_credentials end if payload = login_error_check(user) return render json: payload end # Perform second factor authentication second_factor_auth_result = authenticate_second_factor(user) unless second_factor_auth_result.ok Rails.logger.debug "Second factor authentication failed for user: #{user.id}" return render(json: @second_factor_failure_payload) end # Handle active and email confirmed users if user.active && user.email_confirmed? login(user, second_factor_auth_result) else not_activated(user) end end end end