Revision 5425b3f0
Added by Marc Dequènes over 15 years ago
- ID 5425b3f0470e93a44529f3100be771616135b530
lib/cyborghood/mail.rb | ||
---|---|---|
# This class handles RFC3156 signed messages, validates them, and extract orders properly.
|
||
# Encrypted content are not implemented yet.
|
||
module CyborgHood
|
||
class Order
|
||
attr_accessor :ok, :message, :system_message, :user, :commands, :references
|
||
attr_writer = :warn_sender
|
||
|
||
def initialize(ok, message = nil, system_message = nil)
|
||
@ok = ok
|
||
@message = message
|
||
@system_message = system_message
|
||
end
|
||
|
||
def warn_sender
|
||
# send reply message if user is identified or in selected situations
|
||
# (@warn_sender acts as an override)
|
||
@warn_sender || @user
|
||
end
|
||
end
|
||
|
||
class Mail < Delegator
|
||
include ActionMailer::Quoting
|
||
include ActionMailer::Utils
|
||
|
||
MAX_DRIFT_TIME = 600
|
||
|
||
attr_accessor :user
|
||
|
||
def initialize(msg = nil)
|
||
@config = Config.instance
|
||
|
||
... | ... | |
|
||
def parse
|
||
if is_marked?
|
||
logger.info "Replay detected"
|
||
return nil
|
||
return Order.new(false, "Replay detected.")
|
||
end
|
||
|
||
return parse_signed() if is_pgp_signed?
|
||
return parse_encrypted() if is_pgp_encrypted?
|
||
{:ok => false, :msg => "mail not RFC3156 compliant"}.to_ostruct
|
||
# don't parse commands if user is not identified
|
||
return parse_plain if self.user
|
||
|
||
Order.new(false, "Mail not RFC3156 compliant.")
|
||
end
|
||
|
||
def create_reply
|
||
... | ... | |
tmail_reply.from_addrs = TMail::Address.parse(@config.mail.from_address || self.to.first)
|
||
reply = self.class.new(tmail_reply.to_s)
|
||
reply.set_custom_headers
|
||
reply.user = self.user
|
||
reply
|
||
end
|
||
|
||
... | ... | |
relay.set_disposition("inline")
|
||
reply.quoted_printable_body = msg
|
||
reply.sign
|
||
reply.crypt(reply.user.keyFingerPrint) unless reply.user.nil?
|
||
reply
|
||
end
|
||
|
||
... | ... | |
|
||
private
|
||
|
||
def parse_signed
|
||
order = {:ok => false, :msg => "mail not formatted correctly"}
|
||
def parse_plain
|
||
command_txt = nil
|
||
if multipart?
|
||
if parts[0].content_type == "text/plain"
|
||
command_txt = self.parts[0].body
|
||
refs = self.parts.collect{|p| p.dup }
|
||
end
|
||
else
|
||
command_txt = self.body if self.content_type == "text/plain"
|
||
refs = []
|
||
end
|
||
unless command_txt
|
||
order = Order.new(false, "Mail does not contain a proper text part for commands.")
|
||
order.user = self.user
|
||
return order
|
||
end
|
||
|
||
commands = []
|
||
command_txt.each_line do |line|
|
||
line.chomp!
|
||
sline = line.strip
|
||
# skip empty lines and comments
|
||
next if sline == "" or sline[0, 1] == "#"
|
||
# stop processing when detecting message signature
|
||
break if line == "-- "
|
||
|
||
commands << sline
|
||
end
|
||
|
||
logger.debug "Mail OK"
|
||
mark_processed(sig_check.timestamp)
|
||
|
||
order = Order.new(true)
|
||
order.user = self.user
|
||
order.commands = commands
|
||
order.refs = refs
|
||
end
|
||
|
||
def parse_signed
|
||
sigs_check = verify_pgp_signature()
|
||
return order.to_ostruct if sigs_check.nil? or sigs_check.size != 1
|
||
return Order.new(false, "mail not formatted correctly (signed part)") if sigs_check.nil? or sigs_check.size != 1
|
||
|
||
sig_check = sigs_check.first
|
||
if sig_check.status == 0
|
||
logger.info "Mail content was properly signed by key #{sig_check.fingerprint}"
|
||
user = Person.find_by_fingerprint(sig_check.fingerprint)
|
||
if user.nil?
|
||
logger.info "Mail is from an unknown person"
|
||
order[:msg] = "unknown user"
|
||
return Order.new(false, "Mail content tampered or badly signed: " + sig_check.to_s) unless sig_check.status == 0
|
||
|
||
logger.info "Mail content was properly signed by key #{sig_check.fingerprint}"
|
||
user = Person.find_by_fingerprint(sig_check.fingerprint)
|
||
if user.nil?
|
||
order = Order.new(false, "Mail is from an unknown person.")
|
||
order.warn_sender = true
|
||
return order
|
||
end
|
||
|
||
logger.info "Mail is from user #{user.uid} (#{user.cn})"
|
||
self.user = user
|
||
|
||
drift = Time.new.to_i - sig_check.timestamp.to_i
|
||
logger.debug "Signature drift time: #{drift}"
|
||
unless drift.abs < MAX_DRIFT_TIME
|
||
if drift > 0
|
||
order = Order.new(false, "The signature was made too long ago (check your system clock)." +
|
||
" Rejected message to avoid replay attacks.")
|
||
order.user = self.user
|
||
else
|
||
logger.info "Mail is from user #{user.uid} (#{user.cn})"
|
||
|
||
drift = Time.new.to_i - sig_check.timestamp.to_i
|
||
logger.debug "Signature drift time: #{drift}"
|
||
if drift.abs < MAX_DRIFT_TIME
|
||
signed_content = pgp_signed_part()
|
||
if signed_content.multipart?
|
||
if signed_content.parts[0].content_type == "text/plain"
|
||
command_txt = signed_content.parts[0].body
|
||
refs = signed_content.parts.collect{|p| p.dup }
|
||
end
|
||
else
|
||
command_txt = signed_content.body if signed_content.content_type == "text/plain"
|
||
refs = []
|
||
end
|
||
|
||
if command_txt
|
||
commands = []
|
||
command_txt.each_line do |line|
|
||
line.chomp!
|
||
sline = line.strip
|
||
# skip empty lines and comments
|
||
next if sline == "" or sline[0, 1] == "#"
|
||
# stop processing when detecting message signature
|
||
break if line == "-- "
|
||
|
||
commands << sline
|
||
end
|
||
|
||
logger.debug "Mail OK"
|
||
mark_processed(sig_check.timestamp)
|
||
order = {:ok => true, :user => user, :commands => commands, :refs => refs}
|
||
else
|
||
order[:user] = user
|
||
logger.info "Mail does not contain a proper MIME part for commands"
|
||
end
|
||
else
|
||
if drift > 0
|
||
logger.info "Mail rejected as it may be a replay (signature timestamp is too old)"
|
||
order = {:ok => false, :user => user, :msg => "The signature was made too long ago (perhaps your system clock is not up-to-date). Rejected message to avoid replay attacks."}
|
||
else
|
||
# mark message to prevent later replay of the message
|
||
mark_processed(sig_check.timestamp)
|
||
logger.info "Mail rejected as it is coming from the future (and may allow later replay)"
|
||
order = {:ok => false, :user => user, :msg => "The signature was made in the future (perhaps your system clock is not up-to-date). Rejected message to avoid replay attacks."}
|
||
end
|
||
end
|
||
# mark message to prevent later replay of the message
|
||
mark_processed(sig_check.timestamp)
|
||
order = Order.new(false, "The signature was made in the future (check your system clock)." +
|
||
" Rejected message to avoid replay attacks.")
|
||
order.user = self.user
|
||
end
|
||
else
|
||
logger.info "Mail content tampered or badly signed: " + sig_check.to_s
|
||
return nil
|
||
return order
|
||
end
|
||
|
||
order.to_ostruct
|
||
signed_content = pgp_signed_part()
|
||
|
||
# create a fake mail and chain parsing operations
|
||
plain_mail = self.class.new(signed_content)
|
||
plain_mail.user = self.user
|
||
# propagate message_id to be able to mark messages (replay protection)
|
||
plein_mail.message_id = @mail.message_id
|
||
return plain_mail.parse
|
||
end
|
||
|
||
def parse_encrypted
|
||
order = {:ok => false, :msg => "mail not formatted correctly"}
|
||
|
||
catch :notforme do
|
||
begin
|
||
# block is not passed to delegate (limitation ?)
|
||
... | ... | |
|
||
# create a fake mail and chain parsing operations
|
||
clear_mail = self.class.new(clear_message)
|
||
clear_mail.user = self.user
|
||
# propagate message_id to be able to mark messages (replay protection)
|
||
clear_mail.message_id = @mail.message_id
|
||
return clear_mail.parse
|
||
... | ... | |
end
|
||
end
|
||
|
||
order.to_ostruct
|
||
Order.new(false, "Mail not formatted correctly (encrypted part).")
|
||
end
|
||
|
||
def mark_dir
|
postman | ||
---|---|---|
when :ignorable
|
||
end
|
||
end
|
||
if order.nil?
|
||
logger.info "Mail is invalid, ignoring..."
|
||
msg.delete
|
||
next
|
||
elsif not order.ok
|
||
logger.info "Sending reply for rejected message (#{order.msg})"
|
||
mail_reply = mail.create_simple_reject_reply("A message (ID: #{mail.message_id}) apparently from you was rejected for the following reason:\n #{order.msg}")
|
||
mail_reply.crypt(order.user.keyFingerPrint) unless order.user.nil?
|
||
mail_reply.deliver
|
||
result_tag = order.ok ? "SUCCESS" : "FAILURE"
|
||
logger.info "Processing result: #{result_tag} (#{order.message})"
|
||
logger.info "Extra processing information: " + order.system_message if order.system_message
|
||
unless order.ok
|
||
if order.warn_sender
|
||
logger.info "Sending reply for rejected message"
|
||
mail_reply = mail.create_simple_reject_reply("A message (ID: #{mail.message_id}), apparently from you," +
|
||
" was rejected for the following reason:\n #{order.message}")
|
||
mail_reply.deliver
|
||
end
|
||
msg.delete
|
||
next
|
||
end
|
||
... | ... | |
|
||
# create transcript
|
||
logger.debug "Preparing reply"
|
||
reply_txt = "Hello #{order.user.cn},\n\nFollows the transcript of your commands:\n"
|
||
reply_txt = "Hello #{order.user.cn},\n\n"
|
||
reply_txt += order.message + "\n\n" if order.message
|
||
reply_txt += "Follows the transcript of your commands:\n"
|
||
reply_attachments = []
|
||
result_list.each do |result|
|
||
reply_txt << "> #{result.cmd}\n"
|
Also available in: Unified diff
[evol] separate signed mail processing from commands extracting, and improved order