Organize directories
This commit is contained in:
parent
8a91c9b89b
commit
4898382f78
433 changed files with 0 additions and 0 deletions
7
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp.rb
Normal file
7
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP < SecureMailing::Backend
|
||||
def self.active?
|
||||
Setting.get('pgp_integration')
|
||||
end
|
||||
end
|
||||
170
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp/incoming.rb
Normal file
170
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp/incoming.rb
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Incoming < SecureMailing::Backend::Handler
|
||||
attr_accessor :mail, :content_type
|
||||
|
||||
EXPRESSION_ENCRYPTED = %r{application/pgp-encrypted}i.freeze
|
||||
EXPRESSION_SIGNATURE = %r{application/pgp-signature}i.freeze
|
||||
|
||||
def initialize(mail)
|
||||
super()
|
||||
|
||||
@mail = mail
|
||||
@content_type = mail[:mail_instance].content_type
|
||||
end
|
||||
|
||||
def process
|
||||
return unless process?
|
||||
|
||||
initialize_article_preferences
|
||||
decrypt
|
||||
verify_signature
|
||||
log
|
||||
end
|
||||
|
||||
def initialize_article_preferences
|
||||
article_preferences[:security] = {
|
||||
type: 'PGP',
|
||||
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? || encrypted?
|
||||
end
|
||||
|
||||
def signed?(check_content_type = content_type)
|
||||
EXPRESSION_SIGNATURE.match?(check_content_type)
|
||||
end
|
||||
|
||||
def encrypted?(check_content_type = content_type)
|
||||
EXPRESSION_ENCRYPTED.match?(check_content_type)
|
||||
end
|
||||
|
||||
def decrypt
|
||||
return unless encrypted?
|
||||
|
||||
success = false
|
||||
comment = 'Private key for decryption could not be found.'
|
||||
::PGPKeypair.where.not(private_key: [nil, '']).find_each do |cert|
|
||||
begin
|
||||
index = mail[:attachments].index { |file| file[:preferences]['Content-Type'] == 'application/pgp-encrypted' }
|
||||
data = mail[:attachments][index + 1][:data]
|
||||
decrypted_data = Sequoia.decrypt_for(ciphertext: data.chop, recipient: cert.private_key,
|
||||
password: cert.private_key_secret)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
parse_new_mail(decrypted_data)
|
||||
|
||||
success = true
|
||||
comment = cert.email_addresses.join(', ')
|
||||
|
||||
# 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 unless signed?
|
||||
|
||||
success = false
|
||||
comment = 'Certificate for verification could not be found.'
|
||||
|
||||
::PGPKeypair.where.not(public_key: [nil, '']).find_each do |cert|
|
||||
next unless cert.email_addresses.include? mail[:from_email]
|
||||
|
||||
begin
|
||||
index = mail[:attachments].index { |file| file[:preferences]['Mime-Type'] == 'application/pgp-signature' }
|
||||
data = mail[:attachments][index][:data]
|
||||
verified_data = Sequoia.verify_detached_from(plaintext: mail[:mail_instance].body.encoded, signature: data.chop,
|
||||
sender: cert.public_key)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
parse_new_mail(verified_data)
|
||||
|
||||
success = true
|
||||
comment = cert.email_addresses.join(', ')
|
||||
|
||||
# overwrite content_type for signature checking
|
||||
@content_type = mail[:mail_instance].content_type
|
||||
break
|
||||
end
|
||||
|
||||
article_preferences[:security][:sign] = {
|
||||
success: success,
|
||||
comment: comment
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
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: 'PGP',
|
||||
url: "#{mail[:from_email]} -> #{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
|
||||
120
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp/outgoing.rb
Normal file
120
packages/zammad-addon-pgp/src/lib/secure_mailing/pgp/outgoing.rb
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Outgoing < SecureMailing::Backend::Handler
|
||||
def initialize(mail, security)
|
||||
super()
|
||||
|
||||
@mail = mail
|
||||
@security = security
|
||||
end
|
||||
|
||||
def process
|
||||
return unless process?
|
||||
|
||||
if @security[:sign][:success]
|
||||
sign
|
||||
log('sign', 'success')
|
||||
end
|
||||
if @security[:encryption][:success]
|
||||
encrypt
|
||||
log('encryption', 'success')
|
||||
end
|
||||
end
|
||||
|
||||
def process?
|
||||
return false if @security.blank?
|
||||
return false if @security[:type] != 'PGP'
|
||||
|
||||
@security[:sign][:success] || @security[:encryption][:success]
|
||||
end
|
||||
|
||||
def cleanup(mail)
|
||||
part = Mail::Part.new
|
||||
if mail.multipart?
|
||||
if mail.content_type =~ /^(multipart[^;]+)/
|
||||
part.content_type Regexp.last_match(1)
|
||||
else
|
||||
part.content_type 'multipart/mixed'
|
||||
end
|
||||
mail.body.parts.each do |p|
|
||||
part.add_part cleanup(p)
|
||||
end
|
||||
else
|
||||
# retain important headers if present
|
||||
part.content_type mail.content_type
|
||||
part.content_id mail.header['Content-ID'] if mail.header['Content-ID']
|
||||
part.content_disposition mail.content_disposition if mail.content_disposition
|
||||
|
||||
# force base64 encoding
|
||||
part.body Mail::Encodings::Base64.encode(mail.body.to_s)
|
||||
part.body.encoding = 'base64'
|
||||
end
|
||||
part
|
||||
end
|
||||
|
||||
def sign
|
||||
from = @mail.from.first
|
||||
cert = PGPKeypair.for_sender_email_address(from)
|
||||
raise "Unable to find PGP private key for '#{from}'" unless cert
|
||||
|
||||
signature = Sequoia.sign_detached_with(plaintext: @mail.body.encoded, sender: cert.private_key,
|
||||
password: cert.private_key_secret)
|
||||
|
||||
signature_part = Mail::Part.new do
|
||||
content_type 'application/pgp-signature; name="signature.asc"'
|
||||
content_disposition 'attachment; filename="signature.asc"'
|
||||
content_description 'OpenPGP signature'
|
||||
body signature
|
||||
end
|
||||
@mail.add_part signature_part
|
||||
@mail.content_type "multipart/signed; protocol=\"application/pgp-signature\"; micalg=\"pgp-sha512\"; boundary=\"#{@mail.boundary}\""
|
||||
rescue StandardError => e
|
||||
log('sign', 'failed', e.message)
|
||||
raise
|
||||
end
|
||||
|
||||
def encrypt
|
||||
recipients = []
|
||||
recipients += @mail.to if @mail.to
|
||||
recipients += @mail.cc if @mail.cc
|
||||
recipients += @mail.bcc if @mail.bcc
|
||||
|
||||
certificates = PGPKeypair.for_recipient_email_addresses!(recipients)
|
||||
|
||||
encrypted_control = Mail::Part.new do
|
||||
content_type 'application/pgp-encrypted'
|
||||
content_description 'OpenPGP version'
|
||||
body 'Version: 1'
|
||||
end
|
||||
|
||||
plaintext = @mail.encoded
|
||||
encrypted_part = Mail::Part.new do
|
||||
content_type 'application/octet-stream; name="encrypted.asc"'
|
||||
content_disposition 'inline; filename="encrypted.asc"'
|
||||
content_description 'OpenPGP encrypted message'
|
||||
body Sequoia.encrypt_for(plaintext: plaintext, recipients: certificates.map(&:public_key))
|
||||
end
|
||||
@mail.body = nil
|
||||
@mail.add_part encrypted_control
|
||||
@mail.add_part encrypted_part
|
||||
@mail.content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=\"#{@mail.boundary}\""
|
||||
rescue StandardError => e
|
||||
log('encryption', 'failed', e.message)
|
||||
raise
|
||||
end
|
||||
|
||||
def log(action, status, error = nil)
|
||||
HttpLog.create(
|
||||
direction: 'out',
|
||||
facility: 'PGP',
|
||||
url: "#{@mail[:from_email]} -> #{@mail[:to]}",
|
||||
status: status,
|
||||
ip: nil,
|
||||
request: @security,
|
||||
response: { error: error },
|
||||
method: action,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class SecureMailing::PGP::Retry < SecureMailing::Backend::Handler
|
||||
def initialize(article)
|
||||
super()
|
||||
@article = article
|
||||
end
|
||||
|
||||
def process
|
||||
return existing_result if already_processed?
|
||||
|
||||
save_result if retry_succeeded?
|
||||
retry_result
|
||||
end
|
||||
|
||||
def signature_checked?
|
||||
@signature_checked ||= existing_result&.dig('sign', 'success') || false
|
||||
end
|
||||
|
||||
def decrypted?
|
||||
@decrypted ||= existing_result&.dig('encryption', 'success') || false
|
||||
end
|
||||
|
||||
def already_processed?
|
||||
signature_checked? && decrypted?
|
||||
end
|
||||
|
||||
def existing_result
|
||||
@article.preferences['security']
|
||||
end
|
||||
|
||||
def mail
|
||||
@mail ||= begin
|
||||
raw_mail = @article.as_raw.store_file.content
|
||||
Channel::EmailParser.new.parse(raw_mail).tap do |parsed|
|
||||
SecureMailing.incoming(parsed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def retry_result
|
||||
@retry_result ||= mail['x-zammad-article-preferences']['security']
|
||||
end
|
||||
|
||||
def signature_found?
|
||||
return false if signature_checked?
|
||||
|
||||
retry_result['sign']['success']
|
||||
end
|
||||
|
||||
def decryption_succeeded?
|
||||
return false if decrypted?
|
||||
|
||||
retry_result['encryption']['success']
|
||||
end
|
||||
|
||||
def retry_succeeded?
|
||||
return true if signature_found?
|
||||
|
||||
decryption_succeeded?
|
||||
end
|
||||
|
||||
def save_result
|
||||
save_decrypted if decryption_succeeded?
|
||||
@article.preferences['security'] = retry_result
|
||||
@article.save!
|
||||
end
|
||||
|
||||
def save_decrypted
|
||||
@article.content_type = mail['content_type']
|
||||
@article.body = mail['body']
|
||||
|
||||
Store.remove(
|
||||
object: 'Ticket::Article',
|
||||
o_id: @article.id
|
||||
)
|
||||
|
||||
mail[:attachments]&.each do |attachment|
|
||||
filename = attachment[:filename].force_encoding('utf-8')
|
||||
unless filename.force_encoding('UTF-8').valid_encoding?
|
||||
filename = filename.utf8_encode(fallback: :read_as_sanitized_binary)
|
||||
end
|
||||
Store.add(
|
||||
object: 'Ticket::Article',
|
||||
o_id: @article.id,
|
||||
data: attachment[:data],
|
||||
filename: filename,
|
||||
preferences: attachment[:preferences],
|
||||
created_by_id: @article.created_by_id
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue