trabajo-afectivo/lib/secure_mailing/smime/incoming.rb

226 lines
5.9 KiB
Ruby

# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
class SecureMailing::SMIME::Incoming < SecureMailing::Backend::Handler
attr_accessor :mail, :content_type
EXPRESSION_MIME = %r{application/(x-pkcs7|pkcs7)-mime}i.freeze
EXPRESSION_SIGNATURE = %r{(application/(x-pkcs7|pkcs7)-signature|signed-data)}i.freeze
OPENSSL_PKCS7_VERIFY_FLAGS = OpenSSL::PKCS7::NOVERIFY | OpenSSL::PKCS7::NOINTERN
def initialize(mail)
super()
@mail = mail
@content_type = mail[:mail_instance].content_type
end
def process
return if !process?
initialize_article_preferences
decrypt
verify_signature
log
end
def initialize_article_preferences
article_preferences[:security] = {
type: 'S/MIME',
sign: {
success: false,
comment: nil,
},
encryption: {
success: false,
comment: nil,
}
}
end
def article_preferences
@article_preferences ||= begin
key = :'x-zammad-article-preferences'
mail[ key ] ||= {}
mail[ key ]
end
end
def process?
signed? || smime?
end
def signed?(check_content_type = content_type)
EXPRESSION_SIGNATURE.match?(check_content_type)
end
def signed_type
@signed_type ||= begin
# Special wrapped mime-type S/MIME signature check (e.g. for Microsoft Outlook).
if content_type.include?('signed-data') && EXPRESSION_MIME.match?(content_type)
'wrapped'
else
'inline'
end
end
end
def smime?(check_content_type = content_type)
EXPRESSION_MIME.match?(check_content_type)
end
def decrypt
return if !smime?
success = false
comment = 'Unable to find private key to decrypt'
::SMIMECertificate.where.not(private_key: [nil, '']).find_each do |cert|
key = OpenSSL::PKey::RSA.new(cert.private_key, cert.private_key_secret)
begin
decrypted_data = decrypt_p7enc.decrypt(key, cert.parsed)
rescue
next
end
parse_new_mail(decrypted_data)
success = true
comment = cert.subject
if cert.expired?
comment += " (Certificate #{cert.fingerprint} with start date #{cert.not_before_at} and end date #{cert.not_after_at} expired!)"
end
# overwrite content_type for signature checking
@content_type = mail[:mail_instance].content_type
break
end
article_preferences[:security][:encryption] = {
success: success,
comment: comment,
}
end
def verify_signature
return if !signed?
success = false
comment = 'Unable to find certificate for verification'
result = verify_certificate_chain(verify_sign_p7enc.certificates)
if result.present?
success = true
comment = result
if signed_type == 'wrapped'
parse_new_mail(verify_sign_p7enc.data)
end
mail[:attachments].delete_if do |attachment|
signed?(attachment.dig(:preferences, 'Content-Type'))
end
end
article_preferences[:security][:sign] = {
success: success,
comment: comment,
}
end
def verify_certificate_chain(certificates)
return if certificates.blank?
subjects = certificates.map(&:subject).map(&:to_s)
return if subjects.blank?
existing_certs = ::SMIMECertificate.where(subject: subjects).sort_by do |certificate|
# ensure that we have the same order as the certificates in the mail
subjects.index(certificate.subject)
end
return if existing_certs.blank?
if subjects.size > existing_certs.size
Rails.logger.debug { "S/MIME mail signed with chain '#{subjects.join(', ')}' but only found '#{existing_certs.map(&:subject).join(', ')}' in database." }
end
begin
existing_certs_store = OpenSSL::X509::Store.new
existing_certs.each do |existing_cert|
existing_certs_store.add_cert(existing_cert.parsed)
end
success = verify_sign_p7enc.verify(certificates, existing_certs_store, nil, OPENSSL_PKCS7_VERIFY_FLAGS)
return if !success
existing_certs.map do |existing_cert|
result = existing_cert.subject
if existing_cert.expired?
result += " (Certificate #{existing_cert.fingerprint} with start date #{existing_cert.not_before_at} and end date #{existing_cert.not_after_at} expired!)"
end
result
end.join(', ')
rescue => e
Rails.logger.error "Error while verifying mail with S/MIME certificate subjects: #{subjects}"
Rails.logger.error e
nil
end
end
private
def verify_sign_p7enc
@verify_sign_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
end
def decrypt_p7enc
@decrypt_p7enc ||= OpenSSL::PKCS7.read_smime(mail[:raw])
end
def log
%i[sign encryption].each do |action|
result = article_preferences[:security][action]
next if result.blank?
if result[:success]
status = 'success'
elsif result[:comment].blank?
# means not performed
next
else
status = 'failed'
end
HttpLog.create(
direction: 'in',
facility: 'S/MIME',
url: "#{mail[:from]} -> #{mail[:to]}",
status: status,
ip: nil,
request: {
message_id: mail[:message_id],
},
response: article_preferences[:security],
method: action,
created_by_id: 1,
updated_by_id: 1,
)
end
end
def parse_new_mail(new_mail)
mail[:mail_instance].header['Content-Type'] = nil
mail[:mail_instance].header['Content-Disposition'] = nil
mail[:mail_instance].header['Content-Transfer-Encoding'] = nil
mail[:mail_instance].header['Content-Description'] = nil
new_raw_mail = "#{mail[:mail_instance].header}#{new_mail}"
mail_new = Channel::EmailParser.new.parse(new_raw_mail)
mail_new.each do |local_key, local_value|
mail[local_key] = local_value
end
end
end