discourse-md5_authentication/plugin.rb

251 lines
8.3 KiB
Ruby

# 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.12.1
# authors: saint
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
# require 'digest'
after_initialize do
# Define a module to contain the MD5 authentication logic
module LegacyMd5Authentication
# Constants
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def to64(value, length)
result = String.new
length.times do
result << ITOA64[value & 0x3f]
value >>= 6
Rails.logger.debug "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.debug "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
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.debug "MD5 final_digest: #{final_digest}"
1000.times do |i|
ctx1 = Digest::MD5.new
if i & 1 != 0
Rails.logger.debug "AAA"
ctx1.update(password)
else
Rails.logger.debug "BBB"
ctx1.update(final_digest)
end
ctx1.update(salt) if i % 3 != 0
ctx1.update(password) if i % 7 != 0
if i & 1 != 0
Rails.logger.debug "CCC"
ctx1.update(final_digest)
else
Rails.logger.debug "DDD"
ctx1.update(password)
end
final_digest = ctx1.digest
end
Rails.logger.debug "MD6 final_digest: #{final_digest}"
result = String.new
Rails.logger.debug "A result: #{result}"
result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4)
Rails.logger.debug "B result: #{result}"
result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4)
Rails.logger.debug "C result: #{result}"
result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4)
Rails.logger.debug "D result: #{result}"
result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4)
Rails.logger.debug "E result: #{result}"
result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4)
Rails.logger.debug "F result: #{result}"
result << to64(final_digest[11].ord, 2)
Rails.logger.debug "G result: #{result}"
Rails.logger.debug "magic salt result #{magic}#{salt}$#{result}"
"#{magic}#{salt}$#{result}"
end
def verify_gossamer_password(password, legacy_hash)
generated_hash = gossamer_md5_crypt(password, legacy_hash)
generated_hash == legacy_hash
end
end
# Extend the SessionController create method to include our MD5 authentication logic
class ::SessionController < ApplicationController
prepend LegacyMd5Authentication
def create
params.require(:login)
params.require(:password)
return invalid_credentials if params[:password].length > User.max_password_length
user = User.find_by_username_or_email(normalized_login_param)
raise Discourse::ReadOnly if staff_writes_only_mode? && !user&.staff?
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
Rails.logger.debug "Check for MD5 password in custom field"
if custom_password_md5.present?
# MD5 password is present
Rails.logger.debug "MD5 password is present custom_password_md5: #{custom_password_md5} password: #{password}"
if verify_gossamer_password(password, custom_password_md5)
# MD5 matches, so update the user's password to the new one, remove the custom field and ensure user is set to active and approved
Rails.logger.debug "MD5 matches"
# Set password using Discourse's current standards, ensuring correct hashing, with exception check for the same password as that alaedy stored in Discourse
user.password = password
# Set other attributes
user.active = true
user.approved = true
# user.email_confirmed = true
user.approved_at = Time.now
user.approved_by_id = 1
user.custom_fields['custom_password_md5'] = nil
user.save!
# Generate a new token and hash it
token = SecureRandom.hex(20)
token_hash = EmailToken.hash_token(token)
# Create a confirmed e-mail token
EmailToken.create!(
user_id: user.id,
email: user.email,
token_hash: token_hash,
confirmed: true
)
Rails.logger.debug("Generated token for user #{user.username}: #{token}")
# # Initialize UserAuthenticator with user and session
# authenticator = UserAuthenticator.new(user, session)
# # Generate a salted password hash for the new password
# hashed_password = authenticator.password_digest(password)
# Rails.logger.debug "NEW hashed_password #{hashed_password}"
# # Update the user object with all changes
# user.assign_attributes(
# password_hash: hashed_password,
# # salt: authenticator.salt,
# # password_algorithm: authenticator.algorithm_name,
# active: true,
# approved: true,
# custom_fields: { 'custom_password_md5' => nil }
# )
# if user.save
# Rails.logger.debug "User changes saved: #{user.username}"
# else
# Rails.logger.debug "User changes FAILED: #{user.errors.full_messages}"
# invalid_credentials
# return
# end
Rails.logger.debug "Updated user: #{user.id}"
else
# MD5 doesn't match, so we have a failed login attempt.
Rails.logger.debug "MD5 Password incorrect for user: #{user.id}"
invalid_credentials
return
end
elsif !user.confirm_password?(password)
# There is no MD5 password and the password was incorrect.
Rails.logger.debug "Password incorrect for user: #{user.id}"
invalid_credentials
return
end
# If the site requires user approval and the user is not approved yet
if login_not_approved_for?(user)
render json: login_not_approved
return
end
# User signed on with username and password, so let's prevent the invite link
# from being used to log in (if one exists).
Invite.invalidate_for_email(user.email)
# User's password has expired so they need to reset it
if user.password_expired?(password)
render json: { error: "expired", reason: "expired" }
return
end
else
invalid_credentials
return
end
if payload = login_error_check(user)
return render json: payload
end
second_factor_auth_result = authenticate_second_factor(user)
return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok
if user.active && user.email_confirmed?
login(user, second_factor_auth_result)
else
not_activated(user)
end
end
end
end