enmail 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.hound.yml +3 -0
- data/.rubocop.yml +637 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/README.md +115 -0
- data/REQUIREMENTS.md +214 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/rspec +16 -0
- data/bin/setup +8 -0
- data/enmail.gemspec +31 -0
- data/lib/enmail.rb +7 -0
- data/lib/enmail/certificate_finder.rb +75 -0
- data/lib/enmail/config.rb +21 -0
- data/lib/enmail/configuration.rb +80 -0
- data/lib/enmail/enmailable.rb +43 -0
- data/lib/enmail/key.rb +53 -0
- data/lib/enmail/mail_ext/message.rb +18 -0
- data/lib/enmail/version.rb +3 -0
- data/lib/mail/secure/mail_interceptors/pgp.rb +53 -0
- data/lib/mail/secure/models/key.rb +5 -0
- data/lib/mail/secure/pgp_mailable.rb +107 -0
- metadata +138 -0
data/bin/setup
ADDED
data/enmail.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "enmail/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "enmail"
|
8
|
+
spec.version = EnMail::VERSION
|
9
|
+
spec.authors = ["Ronald Tse"]
|
10
|
+
spec.email = ["ronald.tse@ribose.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Encrypted Email in Ruby}
|
13
|
+
spec.description = %q{Encrypted Email in Ruby}
|
14
|
+
spec.homepage = "https://github.com/riboseinc/enmail"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_dependency "mail", "~> 2.6.4"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
30
|
+
spec.add_development_dependency "pry", "~>0.10.3"
|
31
|
+
end
|
data/lib/enmail.rb
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
require "openssl"
|
2
|
+
|
3
|
+
module EnMail
|
4
|
+
class CertificateFinder
|
5
|
+
def initialize(email:)
|
6
|
+
@email = email
|
7
|
+
end
|
8
|
+
|
9
|
+
# Certificate
|
10
|
+
#
|
11
|
+
# This returns an `OpenSSL::X509::Certificate` instnace which
|
12
|
+
# usages the content for the pem certificate. The certificate
|
13
|
+
# name is the dotify version of the email with `pem` ext.
|
14
|
+
#
|
15
|
+
def certificate
|
16
|
+
certificate_instance
|
17
|
+
end
|
18
|
+
|
19
|
+
# Private Key
|
20
|
+
#
|
21
|
+
# This returns an `OpenSSL::PKey::RSA` instnace which usages
|
22
|
+
# the content for keyfile. The keyfile is the dotify version
|
23
|
+
# of the email with `key` ext.
|
24
|
+
#
|
25
|
+
def private_key
|
26
|
+
private_key_instance
|
27
|
+
end
|
28
|
+
|
29
|
+
# Self.find_by_email
|
30
|
+
#
|
31
|
+
# Initialize a new instnace with more readble interface.
|
32
|
+
#
|
33
|
+
def self.find_by_email(email)
|
34
|
+
new(email: email)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
attr_reader :email
|
40
|
+
|
41
|
+
def certificate_instance
|
42
|
+
OpenSSL::X509::Certificate.new(
|
43
|
+
certificate_file(extension: :pem),
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def private_key_instance
|
48
|
+
OpenSSL::PKey::RSA.new(
|
49
|
+
certificate_file(extension: :key),
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def certificate_file(extension:)
|
54
|
+
content_for(
|
55
|
+
[dotify_email, extension.to_s].join("."),
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
def dotify_email
|
60
|
+
@dotify_email ||= email.sub("@", ".")
|
61
|
+
end
|
62
|
+
|
63
|
+
def content_for(filename)
|
64
|
+
File.read(certificate_file_with_path(filename))
|
65
|
+
end
|
66
|
+
|
67
|
+
def certificate_file_with_path(certificate)
|
68
|
+
File.join(certificates_root, certificate)
|
69
|
+
end
|
70
|
+
|
71
|
+
def certificates_root
|
72
|
+
EnMail.configuration.certificates_path
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "enmail/configuration"
|
2
|
+
|
3
|
+
module EnMail
|
4
|
+
module Config
|
5
|
+
def configure
|
6
|
+
if block_given?
|
7
|
+
yield configuration
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= EnMail::Configuration.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Expose config module methods as class level method, so we can
|
17
|
+
# use those method whenever necessary. Specially `configuration`
|
18
|
+
# throughout the gem
|
19
|
+
#
|
20
|
+
extend Config
|
21
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module EnMail
|
2
|
+
class Configuration
|
3
|
+
attr_reader :smime_adapter
|
4
|
+
attr_accessor :sign_message, :certificates_path
|
5
|
+
attr_accessor :sign_key, :encrypt_key
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@sign_message = true
|
9
|
+
@smime_adapter = :openssl
|
10
|
+
end
|
11
|
+
|
12
|
+
# Signable?
|
13
|
+
#
|
14
|
+
# Returns the message signing status, by defualt it will return `true`.
|
15
|
+
# If the user provided a custom configuration for `sign_message` then
|
16
|
+
# it will use that status, so we can easily skip it when desirable.
|
17
|
+
#
|
18
|
+
def signable?
|
19
|
+
sign_message == true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Set smime adapter
|
23
|
+
#
|
24
|
+
# This allows us to set a valid smime adapter, once this has been
|
25
|
+
# set then the gem will use this on to select the correct adapter
|
26
|
+
# class and then use that one to to `sign` a message.
|
27
|
+
#
|
28
|
+
# @param adapter adapter you want to use to sign the message
|
29
|
+
#
|
30
|
+
def smime_adapter=(adapter)
|
31
|
+
if valid_smime_adapter?(adapter)
|
32
|
+
@smime_adapter = adapter
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adapter klass name
|
37
|
+
#
|
38
|
+
# This returns the string class name for the configured smime
|
39
|
+
# adapter. We are lazely loading the adapter so this interface
|
40
|
+
# will return the string verion. Please do not forget to use
|
41
|
+
# `Object.const_get` before invokng any method on it.
|
42
|
+
#
|
43
|
+
def smime_adapter_klass
|
44
|
+
smime_adapter_symbol_to_klass
|
45
|
+
end
|
46
|
+
|
47
|
+
# Default key
|
48
|
+
#
|
49
|
+
# This returns a Key instance with the configured keys.
|
50
|
+
# @return [EnMail::Key] the configured default key attributes.
|
51
|
+
#
|
52
|
+
def defualt_key
|
53
|
+
EnMail::Key.new(sign_key: sign_key, encrypt_key: encrypt_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def valid_smime_adapter?(adapter)
|
59
|
+
smime_adapters.include?(adapter.to_sym)
|
60
|
+
end
|
61
|
+
|
62
|
+
def smime_adapter_symbol_to_klass
|
63
|
+
adapter_klasses.fetch(smime_adapter.to_sym)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Supported smime adapters
|
67
|
+
#
|
68
|
+
# The list of the supported smime adapters, if we add support for
|
69
|
+
# a new smime adapter then please update this list and this way we
|
70
|
+
# can ensure user can configure the gem with supported adapter only
|
71
|
+
#
|
72
|
+
def smime_adapters
|
73
|
+
[:openssl].freeze
|
74
|
+
end
|
75
|
+
|
76
|
+
def adapter_klasses
|
77
|
+
{ openssl: "EnMail::Adapters::OpenSSL" }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module EnMail
|
2
|
+
module EnMailable
|
3
|
+
# Sign a message
|
4
|
+
#
|
5
|
+
# This interface allow us to sign a message while using this gem, this
|
6
|
+
# also forecefully sets the `signable` status true, so it ensures that
|
7
|
+
# the specific message will be signed before sending out.
|
8
|
+
#
|
9
|
+
# @param passphrase [String] the passphrase for the sign key
|
10
|
+
# @param key [EnMail::Key] the key model instance
|
11
|
+
#
|
12
|
+
def sign(passphrase: "", key: nil)
|
13
|
+
@key = key
|
14
|
+
|
15
|
+
unless passphrase.empty?
|
16
|
+
signing_key.passphrase = passphrase
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Signing key
|
21
|
+
#
|
22
|
+
# This returns the signing key when applicable, the default signing
|
23
|
+
# key is configured through an initializer, but we are also allowing
|
24
|
+
# user to provide a custom key when they are invoking an interface.
|
25
|
+
#
|
26
|
+
# @return [EnMail::Key] the key model instance
|
27
|
+
#
|
28
|
+
def signing_key
|
29
|
+
@key || EnMail.configuration.defualt_key
|
30
|
+
end
|
31
|
+
|
32
|
+
# Signing status
|
33
|
+
#
|
34
|
+
# This returns the message signing status based on the user specified
|
35
|
+
# configuration and signing key. If the user enabled sign_message and
|
36
|
+
# provided a valid signing key then this will return true otherwise
|
37
|
+
# false, this can be used before trying to sing a message.
|
38
|
+
#
|
39
|
+
def signable?
|
40
|
+
signing_key.sign_key && EnMail.configuration.signable?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/enmail/key.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
module EnMail
|
2
|
+
class Key
|
3
|
+
# @!attribute [r] sign_key
|
4
|
+
# @return [String] the signing key content
|
5
|
+
#
|
6
|
+
attr_reader :sign_key
|
7
|
+
|
8
|
+
# @!attribute passphrase
|
9
|
+
# @return [String] the signing key passphrase
|
10
|
+
#
|
11
|
+
attr_reader :passphrase
|
12
|
+
|
13
|
+
# @!attribute [r] encrypt_key
|
14
|
+
# @return [String] the encryping key content
|
15
|
+
#
|
16
|
+
attr_reader :encrypt_key
|
17
|
+
|
18
|
+
# @!attribute [r] certificate
|
19
|
+
# @return [String] the certificate content
|
20
|
+
#
|
21
|
+
attr_reader :certificate
|
22
|
+
|
23
|
+
# Initialize a key model with the basic attributes, this expects us
|
24
|
+
# to provided the key/certificate as string and when we actually use
|
25
|
+
# it then the configured adapter will use it as necessary.
|
26
|
+
#
|
27
|
+
# @param :sign_key [String] the signing key content
|
28
|
+
# @param :passphrase [String] the passphrase for encrypted key
|
29
|
+
# @param :encrypt_key [String] the encryping key content
|
30
|
+
# @param :certificate [String] the signing certificate content
|
31
|
+
#
|
32
|
+
# @return [EnMail::Key] - the EnMail::Key model
|
33
|
+
#
|
34
|
+
def initialize(attributes)
|
35
|
+
@sign_key = attributes.fetch(:sign_key, "")
|
36
|
+
@passphrase = attributes.fetch(:passphrase, "")
|
37
|
+
@encrypt_key = attributes.fetch(:encrypt_key, "")
|
38
|
+
@certificate = attributes.fetch(:certificate, "")
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set the passphrase value
|
42
|
+
#
|
43
|
+
# This allow us to set the passphrase after initialization, so if the
|
44
|
+
# user prefere then they can pass the passphrase during the siging /
|
45
|
+
# encrypting steps and we can set that one when necessary
|
46
|
+
#
|
47
|
+
# @param passphrase [String] the passphrase for encrypted key
|
48
|
+
#
|
49
|
+
def passphrase=(passphrase)
|
50
|
+
@passphrase = passphrase
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "mail"
|
2
|
+
require "enmail/enmailable"
|
3
|
+
|
4
|
+
module Mail
|
5
|
+
class Message
|
6
|
+
# Include enmail interfaces
|
7
|
+
#
|
8
|
+
# We are supporting some custom interfaces for the mail instance,
|
9
|
+
# so the user can use `sign`, `encrypt` and `decrypt` directly to
|
10
|
+
# their mail instance.
|
11
|
+
#
|
12
|
+
# The `EnMail::EnMailable` module defines all of the interfaces
|
13
|
+
# to support the above funcitonality, so let's include that and
|
14
|
+
# please check `EnMail::EnMailable` for more details on those.
|
15
|
+
#
|
16
|
+
include EnMail::EnMailable
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module MailInterceptors
|
2
|
+
class PGP
|
3
|
+
|
4
|
+
def self.delivering_email(mail)
|
5
|
+
|
6
|
+
return unless mail.gpg
|
7
|
+
|
8
|
+
options = TrueClass === mail.gpg ? { encrypt: true } : mail.gpg
|
9
|
+
# encrypt and sign are off -> do not encrypt or sign
|
10
|
+
if options.delete(:encrypt)
|
11
|
+
receivers = []
|
12
|
+
receivers += mail.to if mail.to
|
13
|
+
receivers += mail.cc if mail.cc
|
14
|
+
receivers += mail.bcc if mail.bcc
|
15
|
+
|
16
|
+
if options[:sign_as]
|
17
|
+
options[:sign] = true
|
18
|
+
options[:signers] = options.delete(:sign_as)
|
19
|
+
elsif options[:sign]
|
20
|
+
options[:signers] = mail.from
|
21
|
+
end
|
22
|
+
|
23
|
+
# Need to remove any non-SignedPart & non-SignPart
|
24
|
+
mail.body = ''
|
25
|
+
|
26
|
+
mail.add_part Mail::Gpg::VersionPart.new
|
27
|
+
mail.add_part Mail::Gpg::EncryptedPart.new(mail, options.merge({recipients: receivers}))
|
28
|
+
mail.content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{mail.boundary}"
|
29
|
+
mail.body.preamble = options[:preamble] || "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
|
30
|
+
|
31
|
+
elsif options[:sign] || options[:sign_as]
|
32
|
+
|
33
|
+
to_be_signed = Mail::Gpg::SignedPart.build(mail)
|
34
|
+
|
35
|
+
# Need to remove any non-SignedPart & non-SignPart
|
36
|
+
mail.body = ''
|
37
|
+
|
38
|
+
mail.add_part to_be_signed
|
39
|
+
mail.add_part to_be_signed.sign(options)
|
40
|
+
|
41
|
+
mail.content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{mail.boundary}"
|
42
|
+
mail.body.preamble = options[:preamble] || "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
|
43
|
+
end
|
44
|
+
|
45
|
+
puts "pee pee mail@"
|
46
|
+
pp mail
|
47
|
+
|
48
|
+
rescue Exception
|
49
|
+
raise $! if mail.raise_encryption_errors
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Mail::Secure
|
2
|
+
|
3
|
+
# Include this in your mailer class
|
4
|
+
module PGPMailable
|
5
|
+
|
6
|
+
require 'active_support/concern'
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module InstanceMethods
|
10
|
+
|
11
|
+
# TODO: extract this out for use on a lower level - i.e. not specific to
|
12
|
+
# Rails / ActionMailer.
|
13
|
+
# Boolean: true iff we want to enable signing of emails, false iff we
|
14
|
+
# want to disable it.
|
15
|
+
def sign_emails?
|
16
|
+
# TODO: fetch from config
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO: extract this out for use on a lower level - i.e. not specific to
|
20
|
+
# Rails / ActionMailer.
|
21
|
+
# Has the following properties:
|
22
|
+
# - email
|
23
|
+
# - fingerprint
|
24
|
+
# - user ids
|
25
|
+
# - key body
|
26
|
+
def active_key
|
27
|
+
# TODO: fetch from config
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: extract this out for use on a lower level - i.e. not specific to
|
31
|
+
# Rails / ActionMailer.
|
32
|
+
# Implement this!
|
33
|
+
def key_url
|
34
|
+
'IMPLEMENT THIS'
|
35
|
+
end
|
36
|
+
|
37
|
+
# TODO: extract this out for use on a lower level - i.e. not specific to
|
38
|
+
# Rails / ActionMailer.
|
39
|
+
def add_pgp_headers(headers)
|
40
|
+
return headers unless sign_emails? && active_key
|
41
|
+
|
42
|
+
key_fingerprint = active_key.fingerprint
|
43
|
+
|
44
|
+
headers.merge(
|
45
|
+
gpg: {
|
46
|
+
sign: true,
|
47
|
+
sign_as: active_key.email,
|
48
|
+
},
|
49
|
+
|
50
|
+
# https://www.ietf.org/archive/id/draft-josefsson-openpgp-mailnews-header-07.txt
|
51
|
+
'X-PGP-Key': key_url,
|
52
|
+
OpenPGP: {
|
53
|
+
url: key_url,
|
54
|
+
id: key_fingerprint,
|
55
|
+
}.map{|k, v| "#{k}=#{v};"}.join(" ")
|
56
|
+
)
|
57
|
+
|
58
|
+
rescue StandardError => e
|
59
|
+
if Rails.env.test?
|
60
|
+
raise e
|
61
|
+
end
|
62
|
+
Rails.logger.error "[PGPMailable] Error unable to sign emails: #{e.message} #{e.backtrace}"
|
63
|
+
headers
|
64
|
+
end
|
65
|
+
|
66
|
+
# NOTE: This can remain in PGPMailable because it's specific to
|
67
|
+
# ActionMailer.
|
68
|
+
def mail headers={}, &block
|
69
|
+
# puts "what are headers? #{add_pgp_headers(headers).pretty_inspect}"
|
70
|
+
super(add_pgp_headers(headers), &block).tap do |m|
|
71
|
+
# puts m.to_s
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
included do
|
78
|
+
|
79
|
+
def self.apply(base=self)
|
80
|
+
|
81
|
+
# You can't use .prepend on ActionMailer::Base, oh no you can't!
|
82
|
+
add_to = case base
|
83
|
+
when ActionMailer::Base
|
84
|
+
:include
|
85
|
+
else :prepend
|
86
|
+
end
|
87
|
+
|
88
|
+
unless base < InstanceMethods
|
89
|
+
self.send add_to, Mail::Gpg::Rails::ActionMailerPatch::InstanceMethods
|
90
|
+
self.send add_to, InstanceMethods
|
91
|
+
# self.singleton_class.send add_to, Mail::Gpg::Rails::ActionMailerPatch::ClassMethods
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
apply
|
96
|
+
|
97
|
+
# This would override the original .extended
|
98
|
+
# def self.extended(base)
|
99
|
+
# puts " sooo #{self}.extended #{base}"
|
100
|
+
# apply base
|
101
|
+
# super
|
102
|
+
# end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
end
|