dotgpg 0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f409c5254611c493af247f83c3edcdebe6ea3c64
4
+ data.tar.gz: 227a926742f05d41470c4cb8c7d1062df192b92f
5
+ SHA512:
6
+ metadata.gz: a7f1b699d3358e4cc9b2cbcc3a48a4980162faa9e1e771aafeef33147bc98b6aaba66d47bc47e4b8e9f1b019aacecf4f2bcdb8e132c53e5f3d625e7ca4e4b97f
7
+ data.tar.gz: 6f8cf2b0445cee120efba7fbe90966ace4932e59dfe607bf0709f106a93f3ed8ff8f3ad0d9016205e6404b9b359b7a803a1dea86c0a748bee8294e238dc87548
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org/"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ dotenv-gpg (0.2)
5
+ gpgme
6
+ thor
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ binding_of_caller (0.7.2)
12
+ debug_inspector (>= 0.0.1)
13
+ coderay (1.0.9)
14
+ debug_inspector (0.0.2)
15
+ gpgme (2.0.2)
16
+ method_source (0.8.2)
17
+ pry (0.9.12.2)
18
+ coderay (~> 1.0.5)
19
+ method_source (~> 0.8)
20
+ slop (~> 3.4)
21
+ pry-stack_explorer (0.4.9.1)
22
+ binding_of_caller (>= 0.7)
23
+ pry (>= 0.9.11)
24
+ slop (3.4.6)
25
+ thor (0.18.1)
26
+
27
+ PLATFORMS
28
+ ruby
29
+
30
+ DEPENDENCIES
31
+ dotenv-gpg!
32
+ pry
33
+ pry-stack_explorer
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ dotgpg is a tool for backing up and versioning your production secrets securely and easily.
2
+
3
+ Production secrets are things like your cookie encryption keys, database passwords and AWS access keys. All of them have two things in common: your app needs them to runs and no-one else should be able to get to them.
4
+
5
+ Most people do not look after their production secrets well. If you've got them in your source-code, or unencrypted in Dropbox or Google docs you are betraying your users trust. It's too easy for someone else to get at them.
6
+
7
+ Dotgpg aims to be as easy to use as your current solution, but with added encryption. It manages a shared directory of GPG-encrypted files that you can check into git or put in Dropbox. When you deploy the secrets to your servers they are decrypted so that your app can boot without intervention.
8
+
9
+ Getting started
10
+ ---------------
11
+
12
+ If you're a ruby developer, you know the drill. Either `gem install dotgpg` or add `gem "dotgpg"` to your Gemfile.
13
+
14
+ There are also instructions for [use without ruby](#use-without-ruby).
15
+
16
+ #### Mac OS X
17
+
18
+ 1. `brew install gpg`
19
+ 2. `sudo gem install dotgpg`
20
+
21
+ #### Ubuntu
22
+
23
+ 1. `sudo apt-get install ruby1.9`
24
+ 2. `sudo gem install dotgpg`
25
+
26
+ ## Usage
27
+
28
+ #### dotgpg init
29
+
30
+ To get started run `dotgpg init`. Unless you've used GPG before, it will prompt you for a new passphrase. You should make this passphrase as [secure as your SSH passphrase](#security), i.e. 12-20 characters and not just letters.
31
+
32
+ ```
33
+ $ dotgpg init
34
+ Creating a new GPG key: Conrad Irwin <conrad.irwin@gmail.com>
35
+ Passphrase:
36
+ Passphrase confirmation:
37
+ ```
38
+
39
+ #### dotgpg edit
40
+
41
+ To create or edit files, just use `dotgpg edit`. I recommend you use the `.gpg` suffix so that other tools know what these files contain.
42
+
43
+ ```
44
+ $ dotgpg edit production.env.gpg
45
+ [ opens your $EDITOR ]
46
+ ```
47
+
48
+ #### dotgpg cat
49
+
50
+ To read encrypted files, `dotgpg cat` them.
51
+
52
+ ```
53
+ $ dotgpg cat prodution.env.gpg
54
+ GPG passphrase for conrad.irwin@gmail.com:
55
+ ```
56
+
57
+ #### dotgpg add
58
+
59
+ To add other people to your team, you need to `dotgpg add` them. To run this command you need their public key (see `dotgpg key`).
60
+
61
+ ```
62
+ $ dotgpg add
63
+ Paste a public key, then hit <ctrl-d> twice.
64
+ <paste>
65
+ <ctrl-d><ctrl-d>
66
+ ```
67
+
68
+ Once you've added them run `git commit` or let Dropbox work its syncing magic and they'll be able to access the files just like you.
69
+
70
+ #### dotgpg key
71
+
72
+ To be added to a dotgpg directory, you just need to send your GPG public key to someone who already has access. Getting the key is as easy as running `dotgpg key`. Then email/IM someone who already has access (you can see the list with `ls .gpg`).
73
+
74
+ ```
75
+ $ dotgpg key
76
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
77
+ Version: GnuPG v1.4.15 (Darwin)
78
+
79
+ mQENBFK2JfMBCAC8wX7dsWiNX2Ov9akPlz+54Y7n8a3gtdP63CiabW9Ao4614ZDu
80
+ vZWI8GIr1QaqMQOcUnhVe9BU3u3y4TX5ei1rHp4ykKoum606R7oFKS5Q4viob/6W
81
+ rfVND/o/Sh8twY9ZIpOxRq1zqfGmJk/wSTMuM047hhPUDZVf1BNU+lkURTh2qqnL
82
+ ...snip...
83
+ ZQPcmlBEEI4zq+4GzLTTHHM3/rcHHZmi5p9JAK8OxM/Xyc2otF+N/+iGtIIHjD4a
84
+ 0FJjy4jQzl7FsvLbDf0VDbcw6RZkJ5dGXIyaEcNiOkF3UGwDcfg6oLsA7d5lo+3a
85
+ leJCaaNJQBbIOj4QOjFWiZ8ATqLH9nkgawSwOV3xp0MWayCJ3MVnibt4CaI=
86
+ =Vzb6
87
+ -----END PGP PUBLIC KEY BLOCK-----
88
+ ```
89
+
90
+ ## Use without ruby
91
+
92
+ The only person who really needs to use the `dotgpg` executable is the one responsible for adding and removing users from the directory. If you want to use `dotgpg` without requiring everyone to install ruby you can give them these instructions:
93
+
94
+ To export your GPG key, use: `gpg --armor --export EMAIL_ADDRESS`. (If you get an error 'nothing exported', you can generate a new key using the default settings of `gpg --gen-key`.)
95
+
96
+ To read the encrypted files use `gpg --decrypt FILE`.
97
+
98
+ To edit the encrypted files, you'll want to use [vim-gnupg](https://github.com/jamessan/vim-gnupgnumber) and add `autocmd User GnuPG let b:GPGOptions += ["sign"]` to your `~/.vimrc`. Every time a new user is added to the directory, you'll need to sync GPG's public key store with `gpg --import .gpg/*` or you won't be able to save changes.
99
+
100
+ ## Security
101
+
102
+ I'm not a security professional, so please [email me](conrad.irwin@gmail.com) if you have feedback on anything in this section.
103
+
104
+ The files stored in `dotgpg` are unreadable to an attacker provided:
105
+
106
+ 1. A file encrypted by GnuPG cannot be decrypted except by someone with access to a recipient's private key.
107
+ 2. No-one has access to your GPG private key.
108
+
109
+ The former assumption is reasonably strong. I'm willing to accept the tiny risk that there's a bug in GnuPG because it'll make headline news.
110
+
111
+ The latter assumption is reasonably weak. GPG private keys are stored encrypted on your laptop, and the encryption key is based on a passphrase.
112
+
113
+ This means that if someone gets access to your laptop (or a backup) they can easily get your GPG key unless you've chosen a [secure passphrase](https://howsecureismypassword.net/). I consider this acceptable risk because, by default, SSH passwords are easier to crack than GPG passphrases (GPG uses 65536 rounds of SHA-1 while SSH uses a [single round of MD5](http://martin.kleppmann.com/2013/05/24/improving-security-of-ssh-private-keys.html)) and if they can decrypt your SSH key they can read the secrets directly off your production servers.
114
+
115
+ ### Change passphrase
116
+
117
+ If you didn't choose a secure passphrase, you can change it with:
118
+
119
+ ```
120
+ gpg --edit-keys conrad.irwin@gmail.com passwd
121
+ ```
122
+
123
+ If you can't remember your passphrase then you generate a new key with `dotgpg key -n` and ask someone on your team to overwrite your existing key with `dotgpg add -f`.
124
+
125
+ ### Revoking access
126
+
127
+ Occasionally people leave, or stop needing access to dotgpg. To remove them use `dotgpg rm`.
128
+
129
+ ```
130
+ dotgpg rm conrad.irwin@gmail.com
131
+ ```
132
+
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ task :test do
2
+ Dir['./spec/**/*_spec.rb'].each do |f|
3
+ require f
4
+ end
5
+ end
6
+
7
+ task :default => :test
data/bin/dotgpg ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bundle exec ruby
2
+ require "dotgpg"
3
+
4
+ Dotgpg.interactive = true
5
+ Dotgpg::Cli.start(ARGV)
data/dotgpg.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'dotgpg'
3
+ gem.version = '0.3'
4
+
5
+ gem.summary = 'gpg-encrypted backup for your dotenv files'
6
+ gem.description = "Easy management of gpg-encrypted backup files"
7
+
8
+ gem.authors = ['Conrad Irwin']
9
+ gem.email = %w(conrad@bugsnag.com)
10
+ gem.homepage = 'http://github.com/ConradIrwin/dotenv-gpg'
11
+
12
+ gem.license = 'MIT'
13
+
14
+ gem.add_dependency 'thor'
15
+ gem.add_dependency 'gpgme'
16
+
17
+ gem.add_development_dependency 'pry'
18
+ gem.add_development_dependency 'pry-stack_explorer'
19
+
20
+ gem.executables = 'dotgpg'
21
+ gem.files = `git ls-files`.split("\n")
22
+ end
data/lib/dotgpg/cli.rb ADDED
@@ -0,0 +1,194 @@
1
+ class Dotgpg
2
+ class Cli < Thor
3
+ include Thor::Actions
4
+
5
+ class_option "help", type: :boolean, desc: "Show help", aliases: ["-h"]
6
+
7
+ desc "init [DIRECTORY]", "create a new dotgpg directory"
8
+ option :"new-key", type: :boolean, desc: "Force creating a new key", aliases: ["-n"]
9
+ option :email, type: :string, desc: "Use a specific email address", aliases: ["-e"]
10
+ def init(directory=".")
11
+ return if helped?
12
+
13
+ dir = Dotgpg::Dir.new directory
14
+
15
+ if dir.dotgpg.exist?
16
+ fail "#{directory}/.gpg already exists"
17
+ end
18
+
19
+ key = Dotgpg::Key.secret_key(options[:email], options[:"new-key"])
20
+
21
+ info "Initializing new dotgpg directory"
22
+ info " #{directory}/README.md"
23
+ info " #{directory}/.gpg/#{key.email}"
24
+
25
+ FileUtils.mkdir_p(dir.dotgpg)
26
+ FileUtils.cp Pathname.new(__FILE__).dirname.join("template/README.md"), dir.path.join("README.md")
27
+ dir.add_key(key)
28
+ end
29
+
30
+ desc "key", "export your GPG public key in a format that `dotgpg add` will understand"
31
+ option :"new-key", type: :boolean, desc: "Force creating a new key", aliases: ["-n"]
32
+ option :email, type: :string, desc: "Use a specific email address", aliases: ["-e"]
33
+ def key
34
+ return if helped?
35
+
36
+ key = Dotgpg::Key.secret_key(options[:email], options[:"new-key"])
37
+ $stdout.print key.export(armor: true).to_s
38
+ end
39
+
40
+ desc "add [PUBLIC_KEY]", "add a user's public key", aliases: ["-f"]
41
+ option :force, type: :boolean, desc: "Overwrite an existing key with the same email address", aliases: ["-f"]
42
+ def add(file=nil)
43
+ return if helped?
44
+
45
+ dir = Dotgpg::Dir.closest
46
+ fail "not in a dotgpg directory" unless dir
47
+
48
+ key = read_key_file_for_add(file)
49
+ fail "#{file || "<stdin>"}: not a valid GPG key" unless key
50
+
51
+ if dir.has_key?(key) && !options[:force]
52
+ fail "#{dir.key_path(key)}: already exists"
53
+ end
54
+
55
+ info "Adding #{key.name} to #{dir.path}"
56
+ info " #{dir.key_path(key).relative_path_from(dir.path)}"
57
+
58
+ dir.add_key(key)
59
+ rescue GPGME::Error::BadPassphrase => e
60
+ fail e.message
61
+ end
62
+
63
+ desc "rm KEY", "remove a user's public key"
64
+ option :force, type: :boolean, desc: "Succeed silently if the key doesn't exist or is your own secret key", aliases: ["-f"]
65
+ def rm(file=nil)
66
+ return if helped?(file.nil?)
67
+
68
+ dir = Dotgpg::Dir.closest
69
+ fail "not in a dotgpg directory" unless dir
70
+
71
+ key = read_key_file_for_rm(file)
72
+ fail "#{file}: not a valid GPG key" if !key && !options[:force]
73
+
74
+ if key
75
+ if GPGME::Key.find(:secret).include?(key) && !options[:force]
76
+ fail "#{file}: refusing to remove your own secret key"
77
+ end
78
+
79
+ info "Removing #{key.name} from #{dir.path}"
80
+ info "D #{dir.key_path(key).relative_path_from(dir.path)}"
81
+ dir.remove_key(key)
82
+ end
83
+ rescue GPGME::Error::BadPassphrase => e
84
+ fail e.message
85
+ end
86
+
87
+ desc "cat FILES...", "decrypt and print files"
88
+ def cat(*files)
89
+ return if helped?
90
+
91
+ dir = Dotgpg::Dir.closest(*files)
92
+ fail "not in a dotgpg directory" unless dir
93
+
94
+ files.each do |f|
95
+ dir.decrypt f, $stdout
96
+ end
97
+ rescue GPGME::Error::BadPassphrase => e
98
+ fail e.message
99
+ end
100
+
101
+ desc "edit FILES...", "edit and re-encrypt files"
102
+ def edit(*files)
103
+ return if helped?
104
+
105
+ dir = Dotgpg::Dir.closest(*files)
106
+ fail "not in a dotgpg directory" unless dir
107
+
108
+ dir.reencrypt files do |tempfiles|
109
+ if tempfiles.any?
110
+ to_edit = tempfiles.values.map do |temp|
111
+ Shellwords.escape(temp.path)
112
+ end
113
+
114
+ system "#{Dotgpg.editor} #{to_edit.join(" ")}"
115
+ fail "Problem with editor. Not saving changes" unless $?.success?
116
+ end
117
+ end
118
+
119
+ rescue GPGME::Error::BadPassphrase => e
120
+ fail e.message
121
+ end
122
+
123
+ private
124
+
125
+ # If the global --help or -h flag is passed, show help.
126
+ #
127
+ # Should be invoked at the start of every command.
128
+ #
129
+ # @param [Boolean] force force showing help
130
+ # @return [Boolean] help was shown
131
+ def helped?(force=false)
132
+ if options[:help] || force
133
+ invoke :help, @_invocations[self.class]
134
+ true
135
+ end
136
+ end
137
+
138
+ # Print an informational message in interactive mode.
139
+ #
140
+ # @param [String] msg The message to show
141
+ def info(msg)
142
+ if Dotgpg.interactive?
143
+ $stdout.puts msg
144
+ end
145
+ end
146
+
147
+ # Fail with a message.
148
+ #
149
+ # In interactive mode, exits the program with status 1.
150
+ # Otherwise raises a Dotgpg::Failure.
151
+ #
152
+ # @param [String] msg
153
+ def fail(msg)
154
+ if Dotgpg.interactive?
155
+ $stderr.puts msg
156
+ exit 1
157
+ else
158
+ raise Dotgpg::Failure, msg, caller[1]
159
+ end
160
+ end
161
+
162
+ # Read a key from a given file or stdin.
163
+ #
164
+ # @param [nil, String] the file the user specified.
165
+ # @return [nil, GPGME::Key]
166
+ def read_key_file_for_add(file)
167
+ if file.nil?
168
+ if $stdin.tty?
169
+ info "Paste a public key, then hit <ctrl+d> twice."
170
+ key = Dotgpg::Key.read($stdin)
171
+ else
172
+ key = Dotgpg::Key.read($stdin)
173
+ $stdin.reopen "/dev/tty"
174
+ end
175
+ elsif File.readable?(file)
176
+ key = Dotgpg::Key.read(File.read(file))
177
+ end
178
+ end
179
+
180
+ # Read a key from a given file or from the .gpg directory
181
+ #
182
+ # @param [String] the file the user specified
183
+ # @return [nil, GPGME::Key]
184
+ def read_key_file_for_rm(file)
185
+ if !File.exist?(file) && File.exist?(".gpg/" + file)
186
+ file = ".gpg/" + file
187
+ end
188
+
189
+ if File.readable?(file)
190
+ Dotgpg::Key.read(File.read(file))
191
+ end
192
+ end
193
+ end
194
+ end
data/lib/dotgpg/dir.rb ADDED
@@ -0,0 +1,220 @@
1
+ class Dotgpg
2
+ class Dir
3
+
4
+ attr_reader :path
5
+
6
+ # Find the Dotgpg::Dir that contains the given path.
7
+ #
8
+ # If multiple are given only returns the directory if it contains all
9
+ # paths.
10
+ #
11
+ # If no path is given, find the Dotgpg::Dir that contains the current
12
+ # working directory.
13
+ #
14
+ # @param [*Array<String>] paths
15
+ # @return {nil|[Dotgpg::Dir]}
16
+ def self.closest(path=".", *others)
17
+ path = Pathname.new(File.absolute_path(path)).cleanpath
18
+
19
+ result = path.ascend do |parent|
20
+ maybe = Dotgpg::Dir.new(parent)
21
+ break maybe if maybe.dotgpg?
22
+ end
23
+
24
+ if others.any? && closest(*others) != result
25
+ nil
26
+ else
27
+ result
28
+ end
29
+ end
30
+
31
+ # Open a Dotgpg::Dir
32
+ #
33
+ # @param [String] path The location of the directory
34
+ def initialize(path)
35
+ @path = Pathname.new(File.absolute_path(path)).cleanpath
36
+ end
37
+
38
+ # Get the keys currently associated with this directory.
39
+ #
40
+ # @return [Array<GPGME::Key>]
41
+ def known_keys
42
+ dotgpg.each_child.map do |key_file|
43
+ Dotgpg::Key.read key_file.open
44
+ end
45
+ end
46
+
47
+ # Decrypt the contents of path and write to output.
48
+ #
49
+ # The path should be absolute, and may point to outside
50
+ # this directory, though that is not recommended.
51
+ #
52
+ # @param [Pathname] path The file to decrypt
53
+ # @param [IO] output The IO to write to
54
+ # @return [Boolean] false if decryption failed for an understandable reason
55
+ def decrypt(path, output)
56
+ File.open(path) do |f|
57
+ signature = false
58
+ temp = GPGME::Crypto.new.decrypt f, passphrase_callback: Dotgpg.method(:passfunc) do |s|
59
+ signature = s
60
+ end
61
+
62
+ unless ENV["DOTGPG_ALLOW_INJECTION_ATTACK"]
63
+ raise InvalidSignature, "file was not signed" unless signature
64
+ raise InvalidSignature, "signature was incorrect" unless signature.valid?
65
+ raise InvalidSignature, "signed by a stranger" unless known_keys.include?(signature.key)
66
+ end
67
+
68
+ output.write temp.read
69
+ end
70
+ true
71
+ rescue GPGME::Error::NoData, GPGME::Error::DecryptFailed, SystemCallError => e
72
+ Dotgpg.warn path, e
73
+ false
74
+ end
75
+
76
+ # Encrypt the input and write it to the given path.
77
+ #
78
+ # The path should be absolute, and may point to outside
79
+ # this directory, though that is not recommended.
80
+ #
81
+ # @param [Pathname] path The desired destination
82
+ # @param [IO] input The IO containing the plaintext
83
+ # @return [Boolean] false if encryption failed for an understandable reason
84
+ def encrypt(path, input)
85
+ File.open(path, "w") do |f|
86
+ GPGME::Crypto.new.encrypt input, output: f,
87
+ recipients: known_keys,
88
+ armor: true,
89
+ always_trust: true,
90
+ sign: true,
91
+ passphrase_callback: Dotgpg.method(:passfunc),
92
+ signers: known_keys.detect{ |key| GPGME::Key.find(:secret).include?(key) }
93
+ end
94
+ true
95
+ rescue SystemCallError => e
96
+ Dotgpg.warn path, e
97
+ false
98
+ end
99
+
100
+ # Re-encrypts a set of files with the currently known keys.
101
+ #
102
+ # If a block is provided, it can be used to edit the files in
103
+ # their temporary un-encrypted state.
104
+ #
105
+ # @param [Array<Pathname>] files the files to re-encrypt
106
+ # @yieldparam [Hash<Pathname, Tempfile>] the unencrypted files for each param
107
+ def reencrypt(files, &block)
108
+ tempfiles = {}
109
+
110
+ files.uniq.each do |f|
111
+ temp = Tempfile.new([File.basename(f), ".sh"])
112
+ tempfiles[f] = temp
113
+ if File.exist? f
114
+ decrypted = decrypt f, temp
115
+ tempfiles.delete f unless decrypted
116
+ end
117
+ temp.flush
118
+ temp.close(false)
119
+ end
120
+
121
+ yield tempfiles if block_given?
122
+
123
+ tempfiles.each_pair do |f, temp|
124
+ temp.open
125
+ temp.seek(0)
126
+ encrypt f, temp
127
+ end
128
+
129
+ nil
130
+ ensure
131
+ tempfiles.values.each do |temp|
132
+ temp.close(true)
133
+ end
134
+ end
135
+
136
+ # List every GPG-encrypted file in a directory recursively.
137
+ #
138
+ # Assumes the files are armored (non-armored files are hard to detect and
139
+ # dotgpg itself always armors)
140
+ #
141
+ # This is used to decide which files to re-encrypt when adding a user.
142
+ #
143
+ # @param [Pathname] dir
144
+ # @return [Array<Pathname>]
145
+ def all_encrypted_files(dir=path)
146
+ results = []
147
+ dir.each_child do |child|
148
+ if child.directory?
149
+ if !child.symlink? && child != dotgpg
150
+ results += all_encrypted_files(child)
151
+ end
152
+ elsif child.readable?
153
+ if child.read(1024) =~ /-----BEGIN PGP MESSAGE-----/
154
+ results << child
155
+ end
156
+ end
157
+ end
158
+
159
+ results
160
+ end
161
+
162
+ # Does this directory includea key for the given user yet?
163
+ #
164
+ # @param [GPGME::Key]
165
+ # @return [Boolean]
166
+ def has_key?(key)
167
+ File.exist? key_path(key)
168
+ end
169
+
170
+ # Add a given key to the directory
171
+ #
172
+ # Re-encrypts all files to add the new key as a recipient.
173
+ #
174
+ # @param [GPGME::Key]
175
+ def add_key(key)
176
+ reencrypt all_encrypted_files do
177
+ File.write key_path(key), key.export(armor: true).to_s
178
+ end
179
+ end
180
+
181
+ # Remove a given key from a directory
182
+ #
183
+ # Re-encrypts all files so that the removed key no-longer has access.
184
+ #
185
+ # @param [GPGME::Key]
186
+ def remove_key(key)
187
+ reencrypt all_encrypted_files do
188
+ key_path(key).unlink
189
+ end
190
+ end
191
+
192
+ # The path at which a key should be stored
193
+ #
194
+ # (i.e. .gpg/me@cirw.in)
195
+ #
196
+ # @param [GPGME::Key]
197
+ # @return [Pathname]
198
+ def key_path(key)
199
+ dotgpg + key.email
200
+ end
201
+
202
+ # The .gpg directory
203
+ #
204
+ # @return [Pathname]
205
+ def dotgpg
206
+ path + ".gpg"
207
+ end
208
+
209
+ # Does the .gpg directory exist?
210
+ #
211
+ # @return [Boolean]
212
+ def dotgpg?
213
+ dotgpg.directory?
214
+ end
215
+
216
+ def ==(other)
217
+ Dotgpg::Dir === other && other.path == self.path
218
+ end
219
+ end
220
+ end