Module: Mail::Gpg

Defined in:
lib/mail/gpg.rb,
lib/mail/gpg/version.rb,
lib/mail/gpg/sign_part.rb,
lib/mail/gpg/signed_part.rb,
lib/mail/gpg/gpgme_helper.rb,
lib/mail/gpg/version_part.rb,
lib/mail/gpg/message_patch.rb,
lib/mail/gpg/verified_part.rb,
lib/mail/gpg/decrypted_part.rb,
lib/mail/gpg/encrypted_part.rb,
lib/mail/gpg/delivery_handler.rb,
lib/mail/gpg/missing_keys_error.rb,
lib/mail/gpg/mime_signed_message.rb,
lib/mail/gpg/inline_signed_message.rb,
lib/mail/gpg/verify_result_attribute.rb,
lib/mail/gpg/inline_decrypted_message.rb,
lib/mail/gpg/rails/action_mailer_base_patch.rb

Defined Under Namespace

Modules: MessagePatch, Rails, VerifyResultAttribute Classes: DecryptedPart, DeliveryHandler, EncryptedPart, GpgmeHelper, InlineDecryptedMessage, InlineSignedMessage, MimeSignedMessage, MissingKeysError, SignPart, SignedPart, VerifiedPart, VersionPart

Constant Summary collapse

BEGIN_PGP_MESSAGE_MARKER =
/^-----BEGIN PGP MESSAGE-----/
BEGIN_PGP_SIGNED_MESSAGE_MARKER =
/^-----BEGIN PGP SIGNED MESSAGE-----/
VERSION =
"0.4.6"

Class Method Summary collapse

Class Method Details

.construct_mail(cleartext_mail, options, &block) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/mail/gpg.rb', line 108

def self.construct_mail(cleartext_mail, options, &block)
  Mail.new do
    self.perform_deliveries = cleartext_mail.perform_deliveries
    Mail::Gpg.copy_headers cleartext_mail, self
    # necessary?
    if cleartext_mail.message_id
      header['Message-ID'] = cleartext_mail['Message-ID'].value
    end
    instance_eval &block
  end
end

.copy_headers(from, to, overwrite: true) ⇒ Object

copies all header fields from mail in first argument to that given last



185
186
187
188
189
190
191
# File 'lib/mail/gpg.rb', line 185

def self.copy_headers(from, to, overwrite: true)
  from.header.fields.each do |field|
    if overwrite || to.header[field.name].nil?
      to.header[field.name] = field.value
    end
  end
end

.decrypt(encrypted_mail, options = {}) ⇒ Object

options are: :verify: decrypt and verify



67
68
69
70
71
72
73
74
75
# File 'lib/mail/gpg.rb', line 67

def self.decrypt(encrypted_mail, options = {})
  if encrypted_mime?(encrypted_mail)
    decrypt_pgp_mime(encrypted_mail, options)
  elsif encrypted_inline?(encrypted_mail)
    decrypt_pgp_inline(encrypted_mail, options)
  else
    raise EncodingError, "Unsupported encryption format '#{encrypted_mail.content_type}'"
  end
end

.decrypt_pgp_inline(encrypted_mail, options) ⇒ Object

decrypts inline PGP encrypted mail



138
139
140
# File 'lib/mail/gpg.rb', line 138

def self.decrypt_pgp_inline(encrypted_mail, options)
  InlineDecryptedMessage.setup(encrypted_mail, options)
end

.decrypt_pgp_mime(encrypted_mail, options) ⇒ Object

decrypts PGP/MIME (RFC 3156, section 4) encrypted mail



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/mail/gpg.rb', line 121

def self.decrypt_pgp_mime(encrypted_mail, options)
  if encrypted_mail.parts.length < 2
    raise EncodingError, "RFC 3156 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'"
  end
  if !VersionPart.isVersionPart? encrypted_mail.parts[0]
    raise EncodingError, "RFC 3156 first part not a valid version part '#{encrypted_mail.parts[0]}'"
  end
  decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
  Mail.new(decrypted.raw_source) do
    # headers from the encrypted part (set by the initializer above) take
    # precedence over those from the outer mail.
    Mail::Gpg.copy_headers encrypted_mail, self, overwrite: false
    verify_result decrypted.verify_result if options[:verify]
  end
end

.encrypt(cleartext_mail, options = {}) ⇒ Object

options are: :sign: sign message using the sender's private key :sign_as: sign using this key (give the corresponding email address or key fingerprint) :password: passphrase for the signing key :keys: A hash mapping recipient email addresses to public keys or public key ids. Imports any keys given here that are not already part of the local keychain before sending the mail. :always_trust: send encrypted mail to untrusted receivers, true by default



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/mail/gpg.rb', line 31

def self.encrypt(cleartext_mail, options = {})
  construct_mail(cleartext_mail, options) do
    receivers = []
    receivers += cleartext_mail.to if cleartext_mail.to
    receivers += cleartext_mail.cc if cleartext_mail.cc
    receivers += cleartext_mail.bcc if cleartext_mail.bcc

    if options[:sign_as]
      options[:sign] = true
      options[:signers] = options.delete(:sign_as)
    elsif options[:sign]
      options[:signers] = cleartext_mail.from
    end

    add_part VersionPart.new
    add_part EncryptedPart.new(cleartext_mail,
                               options.merge({recipients: receivers}))
    content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
    body.preamble = options[:preamble] || "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
  end
end

.encrypted?(mail) ⇒ Boolean

true if a mail is encrypted

Returns:

  • (Boolean)


88
89
90
91
92
# File 'lib/mail/gpg.rb', line 88

def self.encrypted?(mail)
  return true if encrypted_mime?(mail)
  return true if encrypted_inline?(mail)
  false
end

.encrypted_inline?(mail) ⇒ Boolean

check if inline PGP (i.e. if any parts of the mail includes the PGP MESSAGE marker)

Returns:

  • (Boolean)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/mail/gpg.rb', line 203

def self.encrypted_inline?(mail)
  return true if mail.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER rescue nil
  if mail.multipart?
    mail.parts.each do |part|
      return true if part.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER rescue nil
      return true if part.has_content_type? &&
        /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type &&
        /.*\.(?:pgp|gpg|asc)$/ =~ part.content_type_parameters[:name] &&
        'signature.asc' != part.content_type_parameters[:name]
      # that last condition above prevents false positives in case e.g.
      # someone forwards a mime signed mail including signature.
    end
  end
  false
end

.encrypted_mime?(mail) ⇒ Boolean

check if PGP/MIME encrypted (RFC 3156)

Returns:

  • (Boolean)


195
196
197
198
199
# File 'lib/mail/gpg.rb', line 195

def self.encrypted_mime?(mail)
  mail.has_content_type? &&
    'multipart/encrypted' == mail.mime_type &&
    'application/pgp-encrypted' == mail.content_type_parameters[:protocol]
end

.sign(cleartext_mail, options = {}) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
# File 'lib/mail/gpg.rb', line 53

def self.sign(cleartext_mail, options = {})
  options[:sign_as] ||= cleartext_mail.from
  construct_mail(cleartext_mail, options) do
    to_be_signed = SignedPart.build(cleartext_mail)
    add_part to_be_signed
    add_part to_be_signed.sign(options)

    content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
    body.preamble = options[:preamble] || "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
  end
end

.signature_valid?(signed_mail, options = {}) ⇒ Boolean

Returns:

  • (Boolean)


77
78
79
80
81
82
83
84
85
# File 'lib/mail/gpg.rb', line 77

def self.signature_valid?(signed_mail, options = {})
  if signed_mime?(signed_mail)
    signature_valid_pgp_mime?(signed_mail, options)
  elsif signed_inline?(signed_mail)
    signature_valid_inline?(signed_mail, options)
  else
    raise EncodingError, "Unsupported signature format '#{signed_mail.content_type}'"
  end
end

.signature_valid_inline?(signed_mail, options) ⇒ Boolean

check signature for inline signed mail

Returns:

  • (Boolean)


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/mail/gpg.rb', line 164

def self.signature_valid_inline?(signed_mail, options)
  result = nil
  if signed_mail.multipart?
    signed_mail.parts.each do |part|
      if signed_inline?(part)
        if result.nil?
          result = true
          signed_mail.verify_result = []
        end
        result &= signature_valid_inline?(part, options)
        signed_mail.verify_result << part.verify_result
      end
    end
  else
    result, verify_result = GpgmeHelper.inline_verify(signed_mail.body.to_s, options)
    signed_mail.verify_result = verify_result
  end
  return result
end

.signature_valid_pgp_mime?(signed_mail, options) ⇒ Boolean

check signature for PGP/MIME (RFC 3156, section 5) signed mail

Returns:

  • (Boolean)


153
154
155
156
157
158
159
160
161
# File 'lib/mail/gpg.rb', line 153

def self.signature_valid_pgp_mime?(signed_mail, options)
  # MUST contain exactly two body parts
  if signed_mail.parts.length != 2
    raise EncodingError, "RFC 3156 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
  end
  result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
  signed_mail.verify_result = verify_result
  return result
end

.signed?(mail) ⇒ Boolean

true if a mail is signed.

throws EncodingError if called on an encrypted mail (so only call this method if encrypted? is false)

Returns:

  • (Boolean)


97
98
99
100
101
102
103
104
# File 'lib/mail/gpg.rb', line 97

def self.signed?(mail)
  return true if signed_mime?(mail)
  return true if signed_inline?(mail)
  if encrypted?(mail)
    raise EncodingError, 'Unable to determine signature on an encrypted mail, use :verify option on decrypt()'
  end
  false
end

.signed_inline?(mail) ⇒ Boolean

check if inline PGP (i.e. if any parts of the mail includes the PGP SIGNED marker)

Returns:

  • (Boolean)


228
229
230
231
232
233
234
235
236
# File 'lib/mail/gpg.rb', line 228

def self.signed_inline?(mail)
  return true if mail.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER rescue nil
  if mail.multipart?
    mail.parts.each do |part|
      return true if part.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER rescue nil
    end
  end
  false
end

.signed_mime?(mail) ⇒ Boolean

check if PGP/MIME signed (RFC 3156)

Returns:

  • (Boolean)


220
221
222
223
224
# File 'lib/mail/gpg.rb', line 220

def self.signed_mime?(mail)
  mail.has_content_type? &&
    'multipart/signed' == mail.mime_type &&
    'application/pgp-signature' == mail.content_type_parameters[:protocol]
end

.verify(signed_mail, options = {}) ⇒ Object



142
143
144
145
146
147
148
149
150
# File 'lib/mail/gpg.rb', line 142

def self.verify(signed_mail, options = {})
  if signed_mime?(signed_mail)
    Mail::Gpg::MimeSignedMessage.setup signed_mail, options
  elsif signed_inline?(signed_mail)
    Mail::Gpg::InlineSignedMessage.setup signed_mail, options
  else
    signed_mail
  end
end