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.
@@ -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