123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class SecureMailing::PGP::Outgoing < SecureMailing::Backend::HandlerOutgoing
- def type
- 'PGP'
- end
- def signed
- raise "Unable to find pgp private key for '#{from}'" if sign_key.nil?
- sign_key.expired!
- construct_signed_mail
- rescue => e
- log('sign', 'failed', e.message)
- raise
- end
- def encrypt(data)
- expired_key = keys.detect(&:expired?)
- raise "Expired key (fingerprint #{sign_key.fingerprint}) at #{expired_key.expires_at} present" if expired_key.present?
- construct_encrypted_mail(data)
- rescue => e
- log('encryption', 'failed', e.message)
- raise
- end
- def self.encoded_body_part(data)
- Mail::Part.new do
- if data.multipart?
- if data.content_type =~ %r{(multipart[^;]+)}
- # preserve multipart/alternative etc
- content_type $1
- else
- content_type 'multipart/mixed'
- end
- data.body.parts.each do |part|
- add_part SecureMailing::PGP::Outgoing.encoded_body_part(part)
- end
- else
- content_type data.content_type
- if data.content_disposition.present?
- content_disposition data.content_disposition
- end
- if data.header['Content-ID'].present?
- content_id data.header['Content-ID']
- end
- # brute force approach to avoid messed up line endings that break signatures with mail 2.7
- body Base64.encode64(data.body.to_s)
- body.encoding = 'base64'
- end
- end
- end
- private
- def from
- mail.from.first
- end
- def sign_key
- sign_key = PGPKey.find_by_uid(from, secret: true)
- return sign_key if sign_key.present?
- nil
- end
- def construct_signed_mail
- signed_mail = Mail.new(mail)
- signed_mail.body = nil
- signed_mail.body.preamble = 'This is an OpenPGP/MIME signed message (RFC 3156)' # rubocop:disable Zammad/DetectTranslatableString
- signed_mail.content_type = "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
- signed_mail.add_part self.class.encoded_body_part(mail)
- signed_mail.add_part signature_part(signed_mail.encoded)
- signed_mail
- end
- def signature_part(data)
- sign_data = nil
- data.match(%r{boundary="(?<boundary>.+)"}) do |match|
- sign_data = data.split("--#{match['boundary']}")[1..-2].join("\r\n--#{match['boundary']}\r\n").strip
- sign_data = "#{sign_data}\r\n"
- end
- signature = signature(sign_data)
- Mail::Part.new do
- body signature
- content_type 'application/pgp-signature; name="signature.asc"'
- content_disposition 'attachment; filename="signature.asc"'
- content_description 'OpenPGP digital signature' # rubocop:disable Zammad/DetectTranslatableString
- end
- end
- def signature(data)
- SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
- pgp_tool.import(sign_key.key)
- result = pgp_tool.sign(data, sign_key.fingerprint, sign_key.passphrase)
- result[:stdout]
- end
- end
- def boundary
- @boundary ||= Mail.random_tag
- end
- def construct_encrypted_mail(data)
- encrypted_mail = Mail.new(data)
- existing_mail_body = existing_mail_body(encrypted_mail)
- encrypted_mail.body = nil
- encrypted_mail.body.preamble = 'This is an OpenPGP/MIME encrypted message (RFC 3156)' # rubocop:disable Zammad/DetectTranslatableString
- encrypted_mail.content_type = "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
- encrypted_mail.add_part version_part
- encrypted_mail.add_part encrypted_part(encrypted_body(existing_mail_body))
- encrypted_mail
- end
- def existing_mail_body(encrypted_mail)
- <<~BODY
- Content-Type: #{encrypted_mail.header['Content-Type']}
- Content-Transfer-Encoding: #{encrypted_mail.header['Content-Transfer-Encoding']}
- #{encrypted_mail.body}
- BODY
- end
- def version_part
- Mail::Part.new do
- body "Version: 1\n" # rubocop:disable Zammad/DetectTranslatableString
- content_type 'application/pgp-encrypted'
- content_description 'PGP/MIME Versions Identification'
- end
- end
- def encrypted_part(data)
- Mail::Part.new do
- body data
- content_type 'application/octet-stream; name="encrypted.asc"'
- content_disposition 'inline; filename="encrypted.asc"'
- content_description 'OpenPGP encrypted message' # rubocop:disable Zammad/DetectTranslatableString
- end
- end
- def encrypted_body(data)
- SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
- keys.each { |key| pgp_tool.import(key.key) }
- encrypted_result = pgp_tool.encrypt(data, keys.map(&:fingerprint))
- encrypted_result[:stdout]
- end
- end
- def keys
- keys = []
- %w[to cc].each do |recipient|
- addresses = mail.send(recipient)
- next if !addresses
- keys += PGPKey.for_recipient_email_addresses!(addresses)
- end
- keys
- end
- end
|