outgoing.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class SecureMailing::PGP::Outgoing < SecureMailing::Backend::HandlerOutgoing
  3. def type
  4. 'PGP'
  5. end
  6. def signed
  7. raise "Unable to find pgp private key for '#{from}'" if sign_key.nil?
  8. sign_key.expired!
  9. construct_signed_mail
  10. rescue => e
  11. log('sign', 'failed', e.message)
  12. raise
  13. end
  14. def encrypt(data)
  15. expired_key = keys.detect(&:expired?)
  16. raise "Expired key (fingerprint #{sign_key.fingerprint}) at #{expired_key.expires_at} present" if expired_key.present?
  17. construct_encrypted_mail(data)
  18. rescue => e
  19. log('encryption', 'failed', e.message)
  20. raise
  21. end
  22. def self.encoded_body_part(data)
  23. Mail::Part.new do
  24. if data.multipart?
  25. if data.content_type =~ %r{(multipart[^;]+)}
  26. # preserve multipart/alternative etc
  27. content_type $1
  28. else
  29. content_type 'multipart/mixed'
  30. end
  31. data.body.parts.each do |part|
  32. add_part SecureMailing::PGP::Outgoing.encoded_body_part(part)
  33. end
  34. else
  35. content_type data.content_type
  36. if data.content_disposition.present?
  37. content_disposition data.content_disposition
  38. end
  39. if data.header['Content-ID'].present?
  40. content_id data.header['Content-ID']
  41. end
  42. # brute force approach to avoid messed up line endings that break signatures with mail 2.7
  43. body Base64.encode64(data.body.to_s)
  44. body.encoding = 'base64'
  45. end
  46. end
  47. end
  48. private
  49. def from
  50. mail.from.first
  51. end
  52. def sign_key
  53. sign_key = PGPKey.find_by_uid(from, secret: true)
  54. return sign_key if sign_key.present?
  55. nil
  56. end
  57. def construct_signed_mail
  58. signed_mail = Mail.new(mail)
  59. signed_mail.body = nil
  60. signed_mail.body.preamble = 'This is an OpenPGP/MIME signed message (RFC 3156)' # rubocop:disable Zammad/DetectTranslatableString
  61. signed_mail.content_type = "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
  62. signed_mail.add_part self.class.encoded_body_part(mail)
  63. signed_mail.add_part signature_part(signed_mail.encoded)
  64. signed_mail
  65. end
  66. def signature_part(data)
  67. sign_data = nil
  68. data.match(%r{boundary="(?<boundary>.+)"}) do |match|
  69. sign_data = data.split("--#{match['boundary']}")[1..-2].join("\r\n--#{match['boundary']}\r\n").strip
  70. sign_data = "#{sign_data}\r\n"
  71. end
  72. signature = signature(sign_data)
  73. Mail::Part.new do
  74. body signature
  75. content_type 'application/pgp-signature; name="signature.asc"'
  76. content_disposition 'attachment; filename="signature.asc"'
  77. content_description 'OpenPGP digital signature' # rubocop:disable Zammad/DetectTranslatableString
  78. end
  79. end
  80. def signature(data)
  81. SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
  82. pgp_tool.import(sign_key.key)
  83. result = pgp_tool.sign(data, sign_key.fingerprint, sign_key.passphrase)
  84. result[:stdout]
  85. end
  86. end
  87. def boundary
  88. @boundary ||= Mail.random_tag
  89. end
  90. def construct_encrypted_mail(data)
  91. encrypted_mail = Mail.new(data)
  92. existing_mail_body = existing_mail_body(encrypted_mail)
  93. encrypted_mail.body = nil
  94. encrypted_mail.body.preamble = 'This is an OpenPGP/MIME encrypted message (RFC 3156)' # rubocop:disable Zammad/DetectTranslatableString
  95. encrypted_mail.content_type = "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
  96. encrypted_mail.add_part version_part
  97. encrypted_mail.add_part encrypted_part(encrypted_body(existing_mail_body))
  98. encrypted_mail
  99. end
  100. def existing_mail_body(encrypted_mail)
  101. <<~BODY
  102. Content-Type: #{encrypted_mail.header['Content-Type']}
  103. Content-Transfer-Encoding: #{encrypted_mail.header['Content-Transfer-Encoding']}
  104. #{encrypted_mail.body}
  105. BODY
  106. end
  107. def version_part
  108. Mail::Part.new do
  109. body "Version: 1\n" # rubocop:disable Zammad/DetectTranslatableString
  110. content_type 'application/pgp-encrypted'
  111. content_description 'PGP/MIME Versions Identification'
  112. end
  113. end
  114. def encrypted_part(data)
  115. Mail::Part.new do
  116. body data
  117. content_type 'application/octet-stream; name="encrypted.asc"'
  118. content_disposition 'inline; filename="encrypted.asc"'
  119. content_description 'OpenPGP encrypted message' # rubocop:disable Zammad/DetectTranslatableString
  120. end
  121. end
  122. def encrypted_body(data)
  123. SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
  124. keys.each { |key| pgp_tool.import(key.key) }
  125. encrypted_result = pgp_tool.encrypt(data, keys.map(&:fingerprint))
  126. encrypted_result[:stdout]
  127. end
  128. end
  129. def keys
  130. keys = []
  131. %w[to cc].each do |recipient|
  132. addresses = mail.send(recipient)
  133. next if !addresses
  134. keys += PGPKey.for_recipient_email_addresses!(addresses)
  135. end
  136. keys
  137. end
  138. end