enmail 0.1.0 → 0.2.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,32 @@
1
+ #!/bin/bash
2
+
3
+ # (c) Copyright 2018 Ribose Inc.
4
+ #
5
+
6
+ # Based on:
7
+ # https://github.com/riboseinc/ruby-rnp/blob/52d6113458cb095cf7811/ci/install.sh
8
+
9
+ set -eux
10
+
11
+ : "${CORES:=2}"
12
+ : "${MAKE:=make}"
13
+
14
+ jsonc_build="${DEPS_BUILD_DIR}/json-c"
15
+
16
+ if [ ! -e "${JSONC_PREFIX}/lib/libjson-c.so" ] && \
17
+ [ ! -e "${JSONC_PREFIX}/lib/libjson-c.dylib" ]; then
18
+
19
+ if [ -d "${jsonc_build}" ]; then
20
+ rm -rf "${jsonc_build}"
21
+ fi
22
+
23
+ mkdir -p "${jsonc_build}"
24
+ pushd ${jsonc_build}
25
+ wget https://s3.amazonaws.com/json-c_releases/releases/json-c-0.12.1.tar.gz -O json-c.tar.gz
26
+ tar xzf json-c.tar.gz --strip 1
27
+
28
+ autoreconf -ivf
29
+ env CFLAGS="-fno-omit-frame-pointer -g" ./configure --prefix="${JSONC_PREFIX}"
30
+ ${MAKE} -j${CORES} install
31
+ popd
32
+ fi
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+
3
+ # (c) Copyright 2018 Ribose Inc.
4
+ #
5
+
6
+ # Based on:
7
+ # https://github.com/riboseinc/ruby-rnp/blob/52d6113458cb095cf7811/ci/install.sh
8
+
9
+ set -eux
10
+
11
+ : "${CORES:=2}"
12
+ : "${MAKE:=make}"
13
+
14
+ rnp_build="${DEPS_BUILD_DIR}/rnp"
15
+
16
+ if [ ! -e "${RNP_PREFIX}/lib/librnp.so" ] && \
17
+ [ ! -e "${RNP_PREFIX}/lib/librnp.dylib" ]; then
18
+
19
+ git clone https://github.com/riboseinc/rnp ${rnp_build}
20
+ pushd "${rnp_build}"
21
+ git checkout "$RNP_VERSION"
22
+ cmake \
23
+ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
24
+ -DBUILD_SHARED_LIBS=yes \
25
+ -DBUILD_TESTING=no \
26
+ -DCMAKE_PREFIX_PATH="${BOTAN_PREFIX};${JSONC_PREFIX}" \
27
+ -DCMAKE_INSTALL_PREFIX="${RNP_PREFIX}" \
28
+ .
29
+ ${MAKE} -j${CORES} install
30
+ popd
31
+ fi
@@ -0,0 +1,68 @@
1
+ = GPGME Adapter Guide
2
+
3
+ `GPGME` adapter provides OpenPGP-compliant cryptography via
4
+ https://gnupg.org/software/gpgme/index.html[GnuPG Made Easy] library.
5
+
6
+ == Dependencies
7
+
8
+ This adapter requries two additional pieces of software to be installed:
9
+
10
+ 1. GnuPG, version 2.2
11
+ 2. `https://rubygems.org/gems/gpgme[gpgme]` gem
12
+
13
+ == Options
14
+
15
+ Following adapter-specific options are supported:
16
+
17
+ `signer`::
18
+ Optional. User id or e-mail which identifies key which will be used for message
19
+ signing. By default, first address from mail's From field is used.
20
+ `key_password`::
21
+ Optional. Password for signer's key. Must be a string.
22
+
23
+ == Non-standard home directory location
24
+
25
+ GnuPG home directory is a place where configuration, keyrings, etc. are stored.
26
+ By default, GnuPG home directory is located in `$HOME/.gnupg`. You can change
27
+ it in a following way:
28
+
29
+ [source,ruby]
30
+ ----
31
+ ::GPGME::Engine.home_dir = 'path/to/home_dir'
32
+ ----
33
+
34
+ Be advised that this setting is global. Hence, if you use GPGME outside EnMail
35
+ as well, your other logic will be affected. One possible workaround is to sign
36
+ e-mails in a different process. This should be fairly easy to achieve in Rails,
37
+ as mailing is often handed to some kind of background job processor which runs
38
+ in its own process. Nevertheless, consider switching to RNP adapter if this
39
+ limitation poses a problem.
40
+
41
+ == Using `gpg.conf`
42
+
43
+ GPGME API accepts little configuration options. Instead, it reads preferences
44
+ from a `gpg.conf` file located in GnuPG home directory (usually `$HOME/.gnupg`).
45
+ You may override defaults there, i.e. set preferred keys or algorithms.
46
+ Refer to GnuPG documentation for
47
+ https://www.gnupg.org/documentation/manuals/gnupg/GPG-Configuration.html[more
48
+ information about configuration files], or for
49
+ https://www.gnupg.org/documentation/manuals/gnupg/GPG-Options.html[list of
50
+ available options]. Also, you will find some nice example `gpg.conf` in this
51
+ https://stackoverflow.com/a/34923350/304175[Stack Overflow answer].
52
+
53
+ == Native extensions
54
+
55
+ The `gpgme` gem includes C extensions.
56
+
57
+ == Issue tracker
58
+
59
+ Bugs, feature requests, and other issues are tracked with `adapter: gpgme`
60
+ label: https://github.com/riboseinc/enmail/issues?q=is%3Aissue+is%3Aopen+label%3A%22adapter%3A+gpgme%22
61
+
62
+ == External links
63
+
64
+ * https://tools.ietf.org/html/rfc1847[RFC 1847 "Security Multiparts for MIME"]
65
+ * https://tools.ietf.org/html/rfc3156[RFC 3156 "MIME Security with OpenPGP"]
66
+ * https://gnupg.org[GNU Privacy Guard home site]
67
+ * https://gnupg.org/software/gpgme/index.html[GPGME (GnuPG Made Easy) library home site]
68
+ * https://github.com/ueno/ruby-gpgme[Ruby bindings for GPGME library]
@@ -0,0 +1,45 @@
1
+ = RNP Adapter Guide
2
+
3
+ `RNP` adapter provides OpenPGP-compliant cryptography via
4
+ https://www.rnpgp.com/[RNP] library.
5
+
6
+ == Dependencies
7
+
8
+ This adapter requries two additional pieces of software to be installed:
9
+
10
+ 1. RNP library, version 0.9.2 or newer
11
+ 2. `https://rubygems.org/gems/rnp[rnp]` gem, version 1.0.1 or newer
12
+
13
+ == Options
14
+
15
+ Following adapter-specific options are supported:
16
+
17
+ `homedir`::
18
+ Optional. Path to RNP home directory, which contains public and secret
19
+ keyrings. In most situations, RNP is able to read GnuPG home directories,
20
+ hence it's common to set it to `<your_home_directory>/.gpg`. Defaults to
21
+ `<your_home_directory>/.rnp`.
22
+ `signer`::
23
+ Optional. User id or e-mail which identifies key which will be used for message
24
+ signing. By default, first address from mail's From field is used.
25
+ `key_password`::
26
+ Optional. Password for signer's key. Can be a string or proc, see
27
+ `rnp` gem documentation for `Rnp#password_provider=`.
28
+
29
+ == Native extensions
30
+
31
+ The `rnp` gem depends on `https://github.com/ffi/ffi[ffi]` gem, which includes
32
+ native extensions.
33
+
34
+ == Issue tracker
35
+
36
+ Bugs, feature requests, and other issues are tracked with `adapter: rnp`
37
+ label: https://github.com/riboseinc/enmail/issues?q=is%3Aissue+is%3Aopen+label%3A%22adapter%3A+rnp%22
38
+
39
+ == External links
40
+
41
+ * https://tools.ietf.org/html/rfc1847[RFC 1847 "Security Multiparts for MIME"]
42
+ * https://tools.ietf.org/html/rfc3156[RFC 3156 "MIME Security with OpenPGP"]
43
+ * https://www.rnpgp.com[RNP library home site]
44
+ * https://github.com/riboseinc/rnp[RNP library on GitHub]
45
+ * https://github.com/riboseinc/ruby-rnp[Ruby bindings for RNP library]
@@ -1,16 +1,21 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path("../lib", __FILE__)
2
+
3
+ # (c) Copyright 2018 Ribose Inc.
4
+ #
5
+
6
+ lib = File.expand_path("lib", __dir__)
3
7
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
8
  require "enmail/version"
9
+ require "enmail/dependency_constraints"
5
10
 
6
11
  Gem::Specification.new do |spec|
7
12
  spec.name = "enmail"
8
13
  spec.version = EnMail::VERSION
9
- spec.authors = ["Ronald Tse"]
10
- spec.email = ["ronald.tse@ribose.com"]
14
+ spec.authors = ["Ribose Inc."]
15
+ spec.email = ["open.source@ribose.com"]
11
16
 
12
- spec.summary = %q{Encrypted Email in Ruby}
13
- spec.description = %q{Encrypted Email in Ruby}
17
+ spec.summary = "Encrypted Email in Ruby"
18
+ spec.description = "Encrypted Email in Ruby"
14
19
  spec.homepage = "https://github.com/riboseinc/enmail"
15
20
  spec.license = "MIT"
16
21
 
@@ -22,10 +27,18 @@ Gem::Specification.new do |spec|
22
27
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
28
  spec.require_paths = ["lib"]
24
29
 
30
+ # There is no reason for 2.6.4 to be a minimum supported version of Mail,
31
+ # except for that this gem was never tested against older versions.
32
+ # Mail 2.6.4 has been released on March 23, 2016, hence should be considered
33
+ # old enough. Nevertheless, pull requests which extend compatibility will be
34
+ # accepted.
25
35
  spec.add_dependency "mail", "~> 2.6.4"
26
36
 
27
- spec.add_development_dependency "bundler", "~> 1.14"
28
- spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "bundler", ">= 1.14", "< 3.0"
38
+ spec.add_development_dependency "gpgme", *EnMail::DependencyConstraints::GPGME
39
+ spec.add_development_dependency "pry", ">= 0.10.3", "< 0.12"
40
+ spec.add_development_dependency "rake", ">= 10", "< 13"
41
+ spec.add_development_dependency "rnp", *EnMail::DependencyConstraints::RNP
29
42
  spec.add_development_dependency "rspec", "~> 3.0"
30
- spec.add_development_dependency "pry", "~>0.10.3"
43
+ spec.add_development_dependency "rspec-pgp_matchers", "~> 0.1.1"
31
44
  end
@@ -1,7 +1,28 @@
1
- require "enmail/key"
2
- require "enmail/config"
3
- require "enmail/certificate_finder"
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ require "mail"
5
+
6
+ require "enmail/version"
7
+ require "enmail/dependency_constraints"
8
+
9
+ require "enmail/helpers/message_manipulation"
10
+ require "enmail/helpers/rfc1847"
11
+ require "enmail/helpers/rfc3156"
12
+
13
+ require "enmail/adapters/base"
14
+ require "enmail/adapters/gpgme"
15
+ require "enmail/adapters/rnp"
16
+
17
+ require "enmail/extensions/message_transport_encoding_restrictions"
4
18
 
5
19
  module EnMail
6
- # Your code goes here...
20
+ module_function
21
+
22
+ def protect(mode, message, adapter:, **options)
23
+ adapter_obj = adapter.new(options)
24
+ adapter_obj.public_send mode, message
25
+ end
7
26
  end
27
+
28
+ Mail::Message.prepend EnMail::Extensions::MessageTransportEncodingRestrictions
@@ -0,0 +1,14 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ module EnMail
5
+ module Adapters
6
+ class Base
7
+ attr_reader :options
8
+
9
+ def initialize(options)
10
+ @options = options
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,81 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ module EnMail
5
+ module Adapters
6
+ # Secures e-mails according to {RFC 3156 "MIME Security with
7
+ # OpenPGP"}[https://tools.ietf.org/html/rfc3156].
8
+ #
9
+ # This adapter uses {GnuPG Made Easy
10
+ # (GPGME)}[https://www.gnupg.org/software/gpgme/index.html] library via
11
+ # interface provided by {gpgme gem}[https://github.com/ueno/ruby-gpgme].
12
+ class GPGME < Base
13
+ include Helpers::MessageManipulation
14
+ include Helpers::RFC1847
15
+ include Helpers::RFC3156
16
+
17
+ def initialize(*args)
18
+ require_relative "gpgme_requirements"
19
+ super
20
+ end
21
+
22
+ private
23
+
24
+ def compute_signature(text, signer)
25
+ plain = ::GPGME::Data.new(text)
26
+ output = ::GPGME::Data.new
27
+ mode = ::GPGME::SIG_MODE_DETACH
28
+ hash_algorithm = nil
29
+
30
+ with_ctx(password: options[:key_password]) do |ctx|
31
+ signer_keys = ::GPGME::Key.find(:secret, signer, :sign)
32
+ ctx.add_signer(*signer_keys)
33
+
34
+ begin
35
+ ctx.sign(plain, output, mode)
36
+ hash_algorithm_num = ctx.sign_result.signatures[0].hash_algo
37
+ hash_algorithm = ::GPGME.hash_algo_name(hash_algorithm_num)
38
+ rescue ::GPGME::Error::UnusableSecretKey => exc
39
+ # TODO Copy-pasted from GPGME gem. Needs any test coverage.
40
+ exc.keys = ctx.sign_result.invalid_signers
41
+ raise exc
42
+ end
43
+ end
44
+
45
+ output.seek(0)
46
+
47
+ ["pgp-#{hash_algorithm.downcase}", output.to_s]
48
+ end
49
+
50
+ def encrypt_string(text, recipients)
51
+ build_crypto.encrypt(text, recipients: recipients)
52
+ end
53
+
54
+ def sign_and_encrypt_string(text, signer, recipients)
55
+ build_crypto.encrypt(
56
+ text,
57
+ sign: true,
58
+ signers: [signer],
59
+ recipients: recipients,
60
+ password: options[:key_password],
61
+ )
62
+ end
63
+
64
+ def build_crypto
65
+ ::GPGME::Crypto.new(default_gpgme_options)
66
+ end
67
+
68
+ def with_ctx(options, &block)
69
+ ctx_options = default_gpgme_options.merge(options)
70
+ ::GPGME::Ctx.new(ctx_options, &block)
71
+ end
72
+
73
+ def default_gpgme_options
74
+ {
75
+ armor: true,
76
+ pinentry_mode: ::GPGME::PINENTRY_MODE_LOOPBACK,
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,3 @@
1
+ gem "gpgme", *EnMail::DependencyConstraints::GPGME
2
+
3
+ require "gpgme"
@@ -0,0 +1,109 @@
1
+ # (c) Copyright 2018 Ribose Inc.
2
+ #
3
+
4
+ module EnMail
5
+ module Adapters
6
+ # Secures e-mails according to {RFC 3156 "MIME Security with
7
+ # OpenPGP"}[https://tools.ietf.org/html/rfc3156].
8
+ #
9
+ # This adapter uses {RNP}[https://www.rnpgp.com/] library via
10
+ # {ruby-rnp gem}[https://github.com/riboseinc/ruby-rnp].
11
+ #
12
+ # NOTE: `Rnp` instances are not thread-safe, and neither this adapter is.
13
+ # Any adapter instance should be accessed by at most one thread at a time.
14
+ class RNP < Base
15
+ include Helpers::MessageManipulation
16
+ include Helpers::RFC1847
17
+ include Helpers::RFC3156
18
+
19
+ attr_reader :rnp
20
+
21
+ def initialize(*args)
22
+ require_relative "rnp_requirements"
23
+ super
24
+ @rnp = build_rnp_and_load_keys
25
+ end
26
+
27
+ private
28
+
29
+ def compute_signature(text, signer)
30
+ signer_key = find_key_for(signer, need_secret: true)
31
+
32
+ signature = rnp.detached_sign(
33
+ signers: [signer_key],
34
+ input: build_input(text),
35
+ armored: true,
36
+ hash: hash_algorithm,
37
+ )
38
+
39
+ ["pgp-#{hash_algorithm.downcase}", signature]
40
+ end
41
+
42
+ def encrypt_string(text, recipients)
43
+ recipient_keys =
44
+ recipients.map { |r| find_key_for(r, need_public: true) }
45
+
46
+ rnp.encrypt(
47
+ recipients: recipient_keys,
48
+ input: build_input(text),
49
+ armored: true,
50
+ )
51
+ end
52
+
53
+ def sign_and_encrypt_string(text, signer, recipients)
54
+ signer_key = find_key_for(signer, need_secret: true)
55
+ recipient_keys =
56
+ recipients.map { |r| find_key_for(r, need_public: true) }
57
+
58
+ rnp.encrypt_and_sign(
59
+ recipients: recipient_keys,
60
+ signers: signer_key,
61
+ input: build_input(text),
62
+ armored: true,
63
+ hash: hash_algorithm,
64
+ )
65
+ end
66
+
67
+ def find_key_for(email, need_public: false, need_secret: false)
68
+ rnp.each_keyid do |keyid|
69
+ key = rnp.find_key(keyid: keyid)
70
+ next if need_public && !key.public_key_present?
71
+ next if need_secret && !key.secret_key_present?
72
+
73
+ key.each_userid do |userid|
74
+ return key if userid.include?(email)
75
+ end
76
+ end
77
+ nil
78
+ end
79
+
80
+ def build_input(text)
81
+ ::Rnp::Input.from_string(text)
82
+ end
83
+
84
+ def build_rnp_and_load_keys
85
+ public_info, secret_info = homedir_info.values_at(:public, :secret)
86
+
87
+ rnp = Rnp.new(public_info[:format], secret_info[:format])
88
+
89
+ [public_info, secret_info].each do |keyring_info|
90
+ input = ::Rnp::Input.from_path(keyring_info[:path])
91
+ rnp.load_keys(format: keyring_info[:format], input: input)
92
+ end
93
+
94
+ rnp.password_provider = options[:key_password]
95
+
96
+ rnp
97
+ end
98
+
99
+ def homedir_info
100
+ @homedir_info ||=
101
+ ::Rnp.homedir_info(options[:homedir] || Rnp.default_homedir)
102
+ end
103
+
104
+ def hash_algorithm
105
+ "SHA512"
106
+ end
107
+ end
108
+ end
109
+ end