enmail 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -0,0 +1,7 @@
1
+ require "enmail/key"
2
+ require "enmail/config"
3
+ require "enmail/certificate_finder"
4
+
5
+ module EnMail
6
+ # Your code goes here...
7
+ end
@@ -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
@@ -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,3 @@
1
+ module EnMail
2
+ VERSION = "0.1.0".freeze
3
+ 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,5 @@
1
+ module Mail::Secure
2
+ class Key
3
+
4
+ end
5
+ 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