dotgpg 0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/bin/dotgpg +5 -0
- data/dotgpg.gemspec +22 -0
- data/lib/dotgpg/cli.rb +194 -0
- data/lib/dotgpg/dir.rb +220 -0
- data/lib/dotgpg/key.rb +105 -0
- data/lib/dotgpg/template/README.md +20 -0
- data/lib/dotgpg.rb +96 -0
- data/spec/cli_spec.rb +255 -0
- data/spec/dir_spec.rb +214 -0
- data/spec/fixture/add1.key +30 -0
- data/spec/fixture/add2.key +30 -0
- data/spec/fixture/add3.key +30 -0
- data/spec/fixture/basic/.gpg/removed1@example.com +30 -0
- data/spec/fixture/basic/.gpg/removed2@example.com +30 -0
- data/spec/fixture/basic/.gpg/removed3@example.com +0 -0
- data/spec/fixture/basic/.gpg/test2@example.com +31 -0
- data/spec/fixture/basic/.gpg/test3@example.com +31 -0
- data/spec/fixture/basic/.gpg/test@example.com +31 -0
- data/spec/fixture/basic/README.md +28 -0
- data/spec/fixture/basic/a +47 -0
- data/spec/fixture/basic/b/c +47 -0
- data/spec/fixture/gnupghome/pubring.gpg +0 -0
- data/spec/fixture/gnupghome/pubring.gpg~ +0 -0
- data/spec/fixture/gnupghome/random_seed +0 -0
- data/spec/fixture/gnupghome/secring.gpg +0 -0
- data/spec/fixture/gnupghome/trustdb.gpg +0 -0
- data/spec/fixture/secret1.key +31 -0
- data/spec/helper/assertions.rb +54 -0
- data/spec/helper.rb +19 -0
- metadata +136 -0
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
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
data/bin/dotgpg
ADDED
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
|