dotenv-gpg 0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ea4591c6118ce5fb0aa7ed03ae6cbb8c5797fd16
4
+ data.tar.gz: 1ec9487f27126c9bca7e44769f2d3e4a6f8b873f
5
+ SHA512:
6
+ metadata.gz: 3e7670a20665fa97c5ae6557f2bc0706d45ad18f98e6b70db2ea9a056c87acd2ab81dfcf30d7144f2e188127db724e288d4b09ad238447e1eb1340c1c0ca6ba7
7
+ data.tar.gz: 18a41ddc5ca53f583d8566e1ba1666758369b81242a1fa1f74ac2d99f442858254b19bfa91906d25606d2f3345210550c91a404ccab6a13a090747c878ee5d9d
@@ -0,0 +1 @@
1
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org/"
2
+ gemspec
@@ -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
@@ -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
+
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bundle exec ruby
2
+ require "dotgpg"
3
+
4
+ Dotgpg.interactive = true
5
+ Dotgpg::Cli.start(ARGV)
@@ -0,0 +1,22 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'dotenv-gpg'
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
@@ -0,0 +1,96 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+ require 'tempfile'
4
+ require 'shellwords'
5
+
6
+ require 'gpgme'
7
+ require 'thor'
8
+
9
+ require "dotgpg/key.rb"
10
+ require "dotgpg/dir.rb"
11
+ require "dotgpg/cli.rb"
12
+
13
+ class Dotgpg
14
+
15
+ class Failure < RuntimeError; end
16
+ class InvalidSignature < RuntimeError; end
17
+
18
+ # This method copied directly from Pry and is
19
+ # Copyright (c) 2013 John Mair (banisterfiend)
20
+ # https://github.com/pry/pry/blob/master/LICENSE
21
+ def self.editor
22
+ configured = ENV["VISUAL"] || ENV["EDITOR"] || guess_editor
23
+ case configured
24
+ when /^mate/, /^subl/
25
+ configured << " -w"
26
+ when /^[gm]vim/
27
+ configured << " --nofork"
28
+ when /^jedit/
29
+ configured << " -wait"
30
+ end
31
+
32
+ configured
33
+ end
34
+
35
+ def self.guess_editor
36
+ %w(subl sublime-text sensible-editor editor mate nano vim vi open).detect do |editor|
37
+ system("which #{editor} > /dev/null 2>&1")
38
+ end
39
+ end
40
+
41
+ def self.read_input(prompt)
42
+ $stderr.print prompt
43
+ $stderr.flush
44
+ $stdin.readline.strip
45
+ end
46
+
47
+ def self.read_passphrase(prompt)
48
+ `stty -echo`
49
+ read_input prompt
50
+ ensure
51
+ $stderr.print "\n"
52
+ `stty echo`
53
+ end
54
+
55
+ def self.interactive=(bool)
56
+ @interactive = bool
57
+ if interactive?
58
+ # get rid of stack trace on <ctrl-c>
59
+ trap(:INT){ exit 2 }
60
+ else
61
+ trap(:INT, "DEFAULT")
62
+ end
63
+ end
64
+
65
+ def self.interactive?
66
+ !!@interactive
67
+ end
68
+
69
+ # TODO: it'd be nice not to store the passphrase in
70
+ # plaintext in RAM.
71
+ def self.passphrase=(passphrase)
72
+ @passphrase = passphrase
73
+ end
74
+
75
+ def self.warn(context, error)
76
+ if interactive?
77
+ $stderr.puts "#{context}: #{error.message}"
78
+ else
79
+ puts "raising warning"
80
+ raise error
81
+ end
82
+ end
83
+
84
+ def self.passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
85
+ if interactive? && (!@passphrase || prev_was_bad != 0)
86
+ uid_hint = $1 if uid_hint =~ /<(.*)>/
87
+ @passphrase = read_passphrase "GPG passphrase for #{uid_hint}: "
88
+ elsif !@passphrase
89
+ raise "You must set Dotgpg.password or Dotgpg.interactive"
90
+ end
91
+
92
+ io = IO.for_fd(fd, 'w')
93
+ io.puts(@passphrase)
94
+ io.flush
95
+ end
96
+ end
@@ -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