123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class SecureMailing::PGP::Incoming < SecureMailing::Backend::HandlerIncoming
- attr_accessor :mime_type, :content_type_parameters
- ENCRYPTION_CONTENT_TYPE = 'application/pgp-encrypted'.freeze
- ENCRYPTED_PART_CONTENT_TYPE = 'application/octet-stream'.freeze
- SIGNATURE_CONTENT_TYPE = 'application/pgp-signature'.freeze
- def initialize(mail)
- super
- @mime_type = mail[:mail_instance].mime_type
- @content_type_parameters = mail[:mail_instance].content_type_parameters
- end
- def type
- 'PGP'
- end
- def encrypted?
- content_type.present? && mime_type.eql?('multipart/encrypted') && content_type_parameters[:protocol].eql?(ENCRYPTION_CONTENT_TYPE)
- end
- def signed?
- content_type.present? && mime_type.eql?('multipart/signed') && content_type_parameters[:protocol].eql?(SIGNATURE_CONTENT_TYPE)
- end
- def decrypt
- return if !decryptable?
- cipher_part = cipher_part_meta_check
- return if cipher_part.nil?
- return if !decrypt_body(cipher_part.body.decoded)
- set_article_preferences(
- operation: :encryption,
- success: true,
- comment: '',
- )
- end
- def verify_signature
- return if !verifiable?
- signature_part = signature_part_meta_check
- return if signature_part.nil?
- verified_result(signature_part.body.decoded)
- set_article_preferences(
- operation: :sign,
- success: true,
- comment: __('Good signature'),
- )
- end
- private
- def update_instance_meta_information
- # Overwrite mime type and content type parameters for decrypted mail.
- @mime_type = mail[:mail_instance].mime_type
- @content_type_parameters = mail[:mail_instance].content_type_parameters
- end
- def result_success?(result)
- result[:status].success?
- end
- def result_comment(result)
- result[:stdout] || result[:stderr] || ''
- end
- def mail_part_check(operation)
- return true if mail[:mail_instance].parts.length.eql?(2)
- set_article_preferences(
- operation: operation,
- comment: __('This PGP email does not have exactly two body parts for PGP mails as mandated by RFC 3156.'),
- )
- false
- end
- def signature_part_meta_check
- signature_part = mail[:mail_instance].parts[1]
- return signature_part if signature_part.has_content_type? && signature_part.mime_type.eql?(SIGNATURE_CONTENT_TYPE)
- set_article_preferences(
- operation: :sign,
- comment: __('The signature part of this PGP email is missing or has a wrong content type according to RFC 3156.'),
- )
- nil
- end
- def version_part_check
- version_part = mail[:mail_instance].parts[0]
- return true if version_part.mime_type.eql?(ENCRYPTION_CONTENT_TYPE) && version_part.body.include?('Version: 1')
- set_article_preferences(
- operation: :encryption,
- comment: __('The first part of this PGP email is not a valid version part as mandated by RFC 3156.'),
- )
- false
- end
- def cipher_part_meta_check
- cipher_part = mail[:mail_instance].parts[1]
- return cipher_part if cipher_part.has_content_type? && cipher_part.mime_type.eql?(ENCRYPTED_PART_CONTENT_TYPE)
- set_article_preferences(
- operation: :encryption,
- comment: __('The encrypted part of this PGP email has an incorrect MIME type according to RFC 3156.'),
- )
- nil
- end
- def verifiable?
- return false if !signed?
- return false if !mail_part_check(:sign)
- return false if sign_keys.blank?
- true
- end
- def verified_result(signature)
- SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
- sign_keys.each { |key| pgp_tool.import(key.key) }
- begin
- pgp_tool.verify(verify_data, signature: signature)
- rescue => e
- set_article_preferences(
- operation: :sign,
- comment: e.message,
- )
- end
- end
- end
- def verify_data
- raw_source = mail['raw']
- parts = raw_source.split(%r{^--#{mail[:mail_instance].boundary}\s$})[1..-2]
- "#{parts[0].strip}\r\n"
- end
- def decryptable?
- return false if !encrypted?
- return false if !mail_part_check(:encryption)
- return false if !version_part_check
- return false if decrypt_keys.blank?
- true
- end
- def decrypt_sign_verify_suppressed?(stderr)
- stderr.include?('gpg: signature verification suppressed')
- end
- def decrypt_body(data)
- result = decrypted_result(data)
- return false if result.nil?
- if !result[:status].success?
- set_article_preferences(
- operation: :encryption,
- comment: result_comment(result)
- )
- return false
- end
- decrypted_body = result[:stdout]
- # If we're not getting a content header, we need to add a newline, otherwise it's fucked up.
- if !decrypted_body.starts_with?(%r{Content-\w+:})
- decrypted_body = "\n#{decrypted_body}"
- end
- parse_decrypted_mail(decrypted_body)
- update_instance_meta_information
- check_signature(result[:stderr])
- true
- end
- def decrypted_result(data)
- SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool| # rubocop:disable Metrics/BlockLength
- result = nil
- decrypt_keys.each do |key|
- pgp_tool.import(key.key)
- begin
- result = pgp_tool.decrypt(data, key.passphrase, skip_verify: true)
- check_signature_embedded(data, key, result[:stderr])
- break
- rescue SecureMailing::PGP::Tool::Error::NoData,
- SecureMailing::PGP::Tool::Error::BadPassphrase,
- SecureMailing::PGP::Tool::Error::NoPassphrase,
- SecureMailing::PGP::Tool::Error::UnknownError => e
- # General decryption errors, no further checks needed.
- set_article_preferences(
- operation: :encryption,
- comment: e.message,
- )
- break
- rescue SecureMailing::PGP::Tool::Error::NoSecretKey
- next
- rescue => e
- set_article_preferences(
- operation: :sign,
- comment: e.message,
- )
- break
- end
- end
- result
- end
- end
- def check_signature_embedded(data, private_key, stderr)
- return if !decrypt_sign_verify_suppressed?(stderr)
- sign_comment = __('Good signature')
- sign_success = true
- SecureMailing::PGP::Tool.new.with_private_keyring do |pgp_tool|
- sign_keys.each { |key| pgp_tool.import(key.key) }
- pgp_tool.import(private_key.key)
- begin
- pgp_tool.decrypt(data, private_key.passphrase)
- rescue SecureMailing::PGP::Tool::Error::NoPublicKey,
- SecureMailing::PGP::Tool::Error::ExpiredKey,
- SecureMailing::PGP::Tool::Error::RevokedKey,
- SecureMailing::PGP::Tool::Error::ExpiredSignature,
- SecureMailing::PGP::Tool::Error::BadSignature,
- SecureMailing::PGP::Tool::Error::ExpiredKeySignature,
- SecureMailing::PGP::Tool::Error::RevokedKeySignature => e
- sign_comment = e.message
- sign_success = false
- end
- end
- set_article_preferences(
- operation: :sign,
- comment: sign_comment,
- success: sign_success,
- )
- end
- def check_signature(result_output)
- return if !signed?
- return if result_output.empty?
- sign_success = false
- sign_comment = ''
- if result_output.include?('gpg: Good signature')
- sign_success = true
- sign_comment = __('Good signature')
- end
- set_article_preferences(
- operation: :sign,
- comment: sign_comment,
- success: sign_success,
- )
- end
- def sign_keys
- @sign_keys ||= pgp_keys(mail[:mail_instance].from.first, :sign, false)
- end
- def decrypt_keys
- @decrypt_keys ||= begin
- keys = []
- mail[:mail_instance].to.each { |to| keys += pgp_keys(to, :encryption, true) }
- if mail[:mail_instance].cc.present?
- mail[:mail_instance].cc.each { |cc| keys += pgp_keys(cc, :encryption, true) }
- end
- keys
- end
- end
- def pgp_keys(uid, operation, secret)
- records = PGPKey.find_all_by_uid(uid, only_valid: false, secret: secret)
- if records.empty?
- set_article_preferences(
- operation: operation,
- comment: secret ? __('The private PGP key could not be found.') : __('The public PGP key could not be found.'),
- )
- return []
- end
- records
- end
- end
|