v0.9.5 -- single class -- checking if this helps us with a couple of issues

This commit is contained in:
dsainty 2024-06-17 18:40:28 +10:00
parent 260a545369
commit bea57091b2

158
plugin.rb
View File

@ -4,7 +4,7 @@
# name: discourse-md5_authentication # name: discourse-md5_authentication
# about: A plugin to authenticate users with MD5 passwords from legacy systems # about: A plugin to authenticate users with MD5 passwords from legacy systems
# version: 0.9.3 # version: 0.9.5
# authors: saint # authors: saint
# url: https://gitea.federated.computer/saint/discourse-md5_authentication.git # url: https://gitea.federated.computer/saint/discourse-md5_authentication.git
@ -12,41 +12,58 @@
after_initialize do after_initialize do
class ::SessionController < ApplicationController class ::SessionController < ApplicationController
# Constants
ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def to64(value, length) def create
result = "" # Require the necessary parameters
length.times do params.require(:login)
result << ITOA64[value & 0x3f] params.require(:password)
value >>= 6
end # Validate password length
result return invalid_credentials if params[:password].length > User.max_password_length
end
# 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 "MD6 password is present. custom_password_md5: #{custom_password_md5}, password: #{password}"
def gossamer_md5_crypt(password, legacy_hash)
# Extract the salt from the legacy hash # Extract the salt from the legacy hash
parts = legacy_hash.split('$') parts = custom_password_md5.split('$')
salt = parts[2] Rails.logger.debug "MD7"
salt = parts[2][0, 8]
# Limit the salt to 8 characters Rails.logger.debug "MD8"
salt = salt[0, 8]
magic = "$GT$" magic = "$GT$"
Rails.logger.debug "MD5 magic: #{magic}" Rails.logger.debug "MD9"
Rails.logger.debug "MD5 magic: #{magic}, salt: #{salt}"
# Create initial MD5 context
ctx = Digest::MD5.new ctx = Digest::MD5.new
ctx.update(password) ctx.update(password)
ctx.update(magic) ctx.update(magic)
ctx.update(salt) ctx.update(salt)
# Create final MD5 digest
final = Digest::MD5.new final = Digest::MD5.new
final.update(password) final.update(password)
final.update(salt) final.update(salt)
final.update(password) final.update(password)
final_digest = final.digest final_digest = final.digest
# Perform password length operations
password_length = password.length password_length = password.length
while password_length > 0 while password_length > 0
ctx.update(final_digest[0, [password_length, 16].min]) ctx.update(final_digest[0, [password_length, 16].min])
password_length -= 16 password_length -= 16
@ -63,8 +80,9 @@ after_initialize do
end end
final_digest = ctx.digest final_digest = ctx.digest
Rails.logger.debug "MD5 final_digest: #{final_digest}" Rails.logger.debug "MD5 final_digest after initial operations: #{final_digest}"
# Perform 1000 iterations of MD5 hashing
1000.times do |i| 1000.times do |i|
ctx1 = Digest::MD5.new ctx1 = Digest::MD5.new
if i & 1 != 0 if i & 1 != 0
@ -82,92 +100,94 @@ after_initialize do
final_digest = ctx1.digest final_digest = ctx1.digest
end end
# Convert final digest to the hashed password format
result = '' result = ''
result << to64((final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord, 4) length = 4
result << to64((final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord, 4) value = (final_digest[0].ord << 16) | (final_digest[6].ord << 8) | final_digest[12].ord
result << to64((final_digest[2].ord << 16) | (final_digest[8].ord << 8) | final_digest[14].ord, 4) length.times do
result << to64((final_digest[3].ord << 16) | (final_digest[9].ord << 8) | final_digest[15].ord, 4) result << ITOA64[value & 0x3f]
result << to64((final_digest[4].ord << 16) | (final_digest[10].ord << 8) | final_digest[5].ord, 4) value >>= 6
result << to64(final_digest[11].ord, 2) end
value = (final_digest[1].ord << 16) | (final_digest[7].ord << 8) | final_digest[13].ord
"#{magic}#{salt}$#{result}" 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 end
def verify_gossamer_password(password, legacy_hash) entered_password_md5 = "#{magic}#{salt}$#{result}"
generated_hash = gossamer_md5_crypt(password, legacy_hash) Rails.logger.debug "Generated MD5 hash: #{entered_password_md5}"
generated_hash == legacy_hash
end
def create # Verify the entered password MD5 hash against the stored hash
params.require(:login) if entered_password_md5 == custom_password_md5
params.require(:password) Rails.logger.debug "MD5 hash matches for user: #{user.id}"
return invalid_credentials if params[:password].length > User.max_password_length # MD5 matches, update the user's password to the new one and remove the custom field
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 and remove the custom field
Rails.logger.debug "MD5 matches"
user.password = password user.password = password
user.custom_fields['custom_password_md5'] = nil user.custom_fields['custom_password_md5'] = nil
user.save! user.save!
Rails.logger.debug "Updated MD5 password for user: #{user.id}" Rails.logger.debug "Updated MD5 password for user: #{user.id}"
else else
# MD5 doesn't match, so we have a failed login attempt. Rails.logger.debug "MD5 password incorrect for user: #{user.id}"
Rails.logger.debug "MD5 Password incorrect for user: #{user.id}" return invalid_credentials
invalid_credentials
return
end end
elsif !user.confirm_password?(password) elsif !user.confirm_password?(password)
# There is no MD5 password and the password was incorrect.
Rails.logger.debug "Password incorrect for user: #{user.id}" Rails.logger.debug "Password incorrect for user: #{user.id}"
invalid_credentials return invalid_credentials
return
end end
# If the site requires user approval and the user is not approved yet # Handle user approval requirements
if login_not_approved_for?(user) if login_not_approved_for?(user)
render json: login_not_approved render json: login_not_approved
return return
end end
# User signed on with username and password, so let's prevent the invite link # Invalidate invite link if user signed on with username and password
# from being used to log in (if one exists).
Invite.invalidate_for_email(user.email) Invite.invalidate_for_email(user.email)
# User's password has expired so they need to reset it # Handle password expiration
if user.password_expired?(password) if user.password_expired?(password)
render json: { error: "expired", reason: "expired" } render json: { error: "expired", reason: "expired" }
return return
end end
else else
invalid_credentials Rails.logger.debug "User not found with login: #{params[:login]}"
return return invalid_credentials
end end
if payload = login_error_check(user) if payload = login_error_check(user)
return render json: payload return render json: payload
end end
# Perform second factor authentication
second_factor_auth_result = authenticate_second_factor(user) second_factor_auth_result = authenticate_second_factor(user)
return render(json: @second_factor_failure_payload) unless second_factor_auth_result.ok 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? if user.active && user.email_confirmed?
login(user, second_factor_auth_result) login(user, second_factor_auth_result)
else else