git-cipher 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/bin/git-cipher +380 -0
  3. metadata +50 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f65b15e499a669209962429243007bbdbbdaaeaa
4
+ data.tar.gz: 21f8abf07fbcb9ef6c7c55c75f78530b965c8815
5
+ SHA512:
6
+ metadata.gz: 614dfcf2bb60ce0f183d20d3c23aa032f58b9b8e5b69bf6fa679c6bd3fd5a94625790099d327526fd7fe353f063fb95646bac1b71cae7ff9bc276005b49f244b
7
+ data.tar.gz: dc7c936e44505b66408eb91c9c19fa736cee2805c5a44e131547ba93382b6ffce0aaf11abe451e983d18b2642f1ce9ae502e9ba148b719627aa37c75d943417d
data/bin/git-cipher ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env ruby
2
+ # git-cipher -- encrypt/decrypt files
3
+
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'shellwords'
7
+
8
+ class Cipher
9
+ EXTENSION = 'encrypted'
10
+ DEFAULT_GPG_USER = 'greg@hurrell.net'
11
+ DEFAULT_GPG_PRESET_COMMAND = '/usr/local/opt/gpg-agent/libexec/gpg-preset-passphrase'
12
+ VALID_OPTIONS = %w[force help]
13
+ VALID_SUBCOMMANDS = %w[decrypt encrypt help preset forget]
14
+
15
+ def run
16
+ send @subcommand
17
+ end
18
+
19
+ private
20
+
21
+ def initialize
22
+ @subcommand, @options, @files = process_args
23
+
24
+ if @options.include?('help') || @subcommand == 'help'
25
+ usage(@subcommand)
26
+ end
27
+
28
+ @force = @options.include?('force')
29
+ end
30
+
31
+ def blue(string)
32
+ colorize(string, 34)
33
+ end
34
+
35
+ def check_ignored(path)
36
+ `#{escape command_path('git')} check-ignore -q #{escape path}`
37
+ puts "[warning: #{path} is not ignored]" unless $?.exitstatus.zero?
38
+ end
39
+
40
+ def colorize(string, color)
41
+ "\e[#{color}m#{string}\e[0m"
42
+ end
43
+
44
+ def command_name
45
+ @command_name ||= begin
46
+ if `ps -p #{Process.ppid.to_i}` =~ /\bgit cipher\b/
47
+ 'git cipher'
48
+ else
49
+ File.basename(__FILE__)
50
+ end
51
+ rescue
52
+ File.basename(__FILE__)
53
+ end
54
+ end
55
+
56
+ def command_path(command)
57
+ path = `command -v #{escape command}`.chomp
58
+ die "required dependency #{command} not found" if path.empty?
59
+ path
60
+ end
61
+
62
+ def decrypt
63
+ if @files.empty?
64
+ puts 'No explicit paths supplied: decrypting all matching files'
65
+ Dir["**/.*.#{EXTENSION}"].each { |file| decrypt!(file) }
66
+ else
67
+ @files.each { |file| decrypt!(file) }
68
+ end
69
+ end
70
+
71
+ def decrypt!(file)
72
+ unless ENV['GPG_AGENT_INFO']
73
+ die <<-MSG
74
+ GPG_AGENT_INFO not present in the environment.
75
+ Try running:
76
+ eval $(gpg-agent --daemon)
77
+ #{command_name} preset
78
+ MSG
79
+ end
80
+
81
+ pathname = Pathname.new(file)
82
+ basename = pathname.basename.to_s
83
+ unless basename.start_with?('.')
84
+ die "#{file} does not begin with a period"
85
+ end
86
+
87
+ unless basename.end_with?(".#{EXTENSION}")
88
+ die "#{file} does not have an .#{EXTENSION} extension"
89
+ end
90
+
91
+ unless pathname.exist?
92
+ die "#{file} does not exist"
93
+ end
94
+
95
+ outfile = pathname.dirname + basename.gsub(
96
+ /\A\.|\.#{EXTENSION}\z/, ''
97
+ )
98
+
99
+ print "#{file} -> #{outfile} "
100
+
101
+ if FileUtils.uptodate?(outfile, [file]) && !@force
102
+ # decrypted is newer than encrypted; it might have local changes which we
103
+ # could blow away, so warn
104
+ puts red('[warning: plain-text newer than ciphertext; skipping]')
105
+ else
106
+ print green('[decrypting ...')
107
+ execute(%{
108
+ #{escape command_path('gpg')}
109
+ -q
110
+ --yes
111
+ --batch
112
+ --no-tty
113
+ --use-agent
114
+ -o #{escape outfile}
115
+ -d #{escape file}
116
+ })
117
+ if $?.success?
118
+ puts green(' done]')
119
+
120
+ File.chmod(0600, outfile)
121
+
122
+ # mark plain-text as older than ciphertext, this will prevent a
123
+ # bin/encrypt run from needlessly changing the contents of the ciphertext
124
+ # (as the encryption is non-deterministic)
125
+ time = File.mtime(file) - 1
126
+ File.utime(time, time, outfile)
127
+ else
128
+ print kill_line
129
+ puts red('[decrypting ... failed; bailing]')
130
+ exit $?.exitstatus
131
+ end
132
+
133
+ check_ignored(outfile)
134
+ end
135
+ end
136
+
137
+ def die(msg)
138
+ STDERR.puts red('error:'), strip_heredoc(msg)
139
+ exit 1
140
+ end
141
+
142
+ def encrypt
143
+ if @files.empty?
144
+ puts 'No explicit paths supplied: encrypting all matching files'
145
+ Dir["**/.*.#{EXTENSION}"].each do |file|
146
+ file = Pathname.new(file)
147
+ encrypt!(
148
+ file.dirname +
149
+ file.basename.to_s.gsub(/\A\.|\.#{EXTENSION}\z/, '')
150
+ )
151
+ end
152
+ else
153
+ @files.each { |file| encrypt!(Pathname.new(file)) }
154
+ end
155
+ end
156
+
157
+ def encrypt!(file)
158
+ unless file.exist?
159
+ die "#{file} does not exist"
160
+ end
161
+
162
+ outfile = file.dirname + ".#{file.basename}.#{EXTENSION}"
163
+
164
+ print "#{file} -> #{outfile} "
165
+ if FileUtils.uptodate?(outfile, [file]) && !@force
166
+ puts blue('[up to date]')
167
+ else
168
+ print green('[encrypting ...')
169
+ execute(%{
170
+ #{escape command_path('gpg')}
171
+ -a
172
+ -q
173
+ --batch
174
+ --no-tty
175
+ --yes
176
+ -r #{escape gpg_user}
177
+ -o #{escape outfile}
178
+ -e #{escape file}
179
+ })
180
+ if $?.success?
181
+ puts green(' done]')
182
+ else
183
+ print kill_line
184
+ puts red('[encrypting ... failed; bailing]')
185
+ exit $?.exitstatus
186
+ end
187
+ end
188
+
189
+ File.chmod(0600, file)
190
+ check_ignored(file)
191
+ end
192
+
193
+ def escape(string)
194
+ Shellwords.escape(string)
195
+ end
196
+
197
+ def execute(string)
198
+ %x{#{string.gsub("\n", ' ')}}
199
+ end
200
+
201
+ def forget
202
+ `#{escape command_path(gpg_preset_command)} --forget #{keygrip}`
203
+ die 'gpg-preset-passphrase failed' unless $?.success?
204
+ end
205
+
206
+ def get_config(key)
207
+ value = `#{escape command_path('git')} config cipher.#{key}`.chomp
208
+ return if value.empty?
209
+ value
210
+ end
211
+
212
+ def get_passphrase
213
+ stty = escape(command_path('stty'))
214
+ print 'Passphrase [will not be echoed]: '
215
+ `#{stty} -echo` # quick hack, cheaper than depending on highline gem
216
+ STDIN.gets.chomp
217
+ ensure
218
+ `#{stty} echo`
219
+ puts
220
+ end
221
+
222
+ def gpg_preset_command
223
+ ENV['GPG_PRESET_COMMAND'] || get_config('presetcommand') || DEFAULT_GPG_PRESET_COMMAND
224
+ end
225
+
226
+ def gpg_user
227
+ ENV['GPG_USER'] || get_config('gpguser') || DEFAULT_GPG_USER
228
+ end
229
+
230
+ def green(string)
231
+ colorize(string, 32)
232
+ end
233
+
234
+ def keygrip
235
+ # get the subkey fingerprint and convert it into a 40-char hex code
236
+ @keygrip ||= execute(%{
237
+ #{escape command_path('gpg')} --fingerprint #{escape gpg_user} |
238
+ grep fingerprint |
239
+ tail -1 |
240
+ cut -d= -f2 |
241
+ sed -e 's/ //g'
242
+ })
243
+ end
244
+
245
+ def kill_line
246
+ # 2K deletes the line, 0G moves to column 0
247
+ # see: http://en.wikipedia.org/wiki/ANSI_escape_code
248
+ "\e[2K\e[0G"
249
+ end
250
+
251
+ def normalize_option(option)
252
+ normal = option.dup
253
+
254
+ if normal.sub!(/\A--/, '') # long option
255
+ found = VALID_OPTIONS.find { |o| o == normal }
256
+ elsif normal.sub!(/\A-/, '') # short option
257
+ found = VALID_OPTIONS.find { |o| o[0] == normal }
258
+ end
259
+
260
+ die "unrecognized option: #{option}" if found.nil?
261
+
262
+ found
263
+ end
264
+
265
+ def preset
266
+ passphrase = get_passphrase
267
+ command = "#{escape command_path(gpg_preset_command)} --preset #{keygrip}"
268
+ IO.popen(command, 'w+') do |io|
269
+ io.puts passphrase
270
+ io.close_write
271
+ puts io.read # usually silent
272
+ end
273
+ die 'gpg-preset-passphrase failed' unless $?.success?
274
+ end
275
+
276
+ def process_args
277
+ options, files = ARGV.partition { |arg| arg.start_with?('-') }
278
+ subcommand = files.shift
279
+
280
+ options.map! { |option| normalize_option(option) }
281
+
282
+ unless VALID_SUBCOMMANDS.include?(subcommand)
283
+ if subcommand.nil?
284
+ message = 'no subcommand'
285
+ else
286
+ message = 'unrecognized subcommand'
287
+ end
288
+ die [message, "expected one of #{VALID_SUBCOMMANDS.inspect}"].join(': ')
289
+ end
290
+
291
+ [subcommand, options, files]
292
+ end
293
+
294
+ def red(string)
295
+ colorize(string, 31)
296
+ end
297
+
298
+ def strip_heredoc(doc)
299
+ # based on method of same name from Rails
300
+ indent = doc.scan(/^[ \t]*(?=\S)/).map(&:size).min || 0
301
+ doc.gsub(/^[ \t]{#{indent}}/, '')
302
+ end
303
+
304
+ # Print usage information and exit.
305
+ def usage(subcommand)
306
+ case subcommand
307
+ when 'decrypt'
308
+ puts strip_heredoc(<<-USAGE)
309
+ #{command_name} decrypt [-f|--force] [FILES...]
310
+
311
+ Decrypts files that have been encrypted for storage in version control
312
+
313
+ Decrypt two files, but only if the corresponding plain-text files
314
+ are missing or older:
315
+
316
+ #{command_name} decrypt .foo.encrypted .bar.encrypted
317
+
318
+ Decrypt all decryptable files:
319
+
320
+ #{command_name} decrypt
321
+
322
+ (Re-)decrypt all decryptable files, even those whose corresponding
323
+ plain-text files are newer:
324
+
325
+ #{command_name} decrypt -f
326
+ #{command_name} decrypt --force # (alternative syntax)
327
+ USAGE
328
+ when 'encrypt'
329
+ puts strip_heredoc(<<-USAGE)
330
+ #{command_name} encrypt [-f|--force] [FILES...]
331
+
332
+ Encrypts files for storage in version control
333
+
334
+ Encrypt two files, but only if the corresponding ciphertext files
335
+ are missing or older:
336
+
337
+ #{command_name} encrypt foo bar
338
+
339
+ Encrypt all encryptable files:
340
+
341
+ #{command_name} encrypt
342
+
343
+ (Re-)encrypt all encryptable files, even those whose corresponding
344
+ ciphertext files are newer:
345
+
346
+ #{command_name} encrypt -f
347
+ #{command_name} encrypt --force # (alternative syntax)
348
+ USAGE
349
+ when 'forget'
350
+ puts strip_heredoc(<<-USAGE)
351
+ #{command_name} forget
352
+
353
+ Forget passphrase previously stored using `#{command_name} preset`:
354
+
355
+ #{command_name} forget
356
+ USAGE
357
+ when 'preset'
358
+ puts strip_heredoc(<<-USAGE)
359
+ #{command_name} preset
360
+
361
+ Store a passphrase in a running `gpg-agent` agent:
362
+
363
+ #{command_name} preset
364
+ USAGE
365
+ else
366
+ puts strip_heredoc(<<-USAGE)
367
+ Available commands (invoke any with -h or --help for more info):
368
+
369
+ #{command_name} decrypt
370
+ #{command_name} encrypt
371
+ #{command_name} forget
372
+ #{command_name} preset
373
+ USAGE
374
+ end
375
+
376
+ exit
377
+ end
378
+ end
379
+
380
+ Cipher.new.run
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git-cipher
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Greg Hurrell
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: "\n Provides a convenient workflow for working with encrypted files
14
+ in a public\n Git repo. Delegates the underlying work of encryption/decryption
15
+ to GnuPG.\n "
16
+ email:
17
+ - greg@hurrell.net
18
+ executables:
19
+ - git-cipher
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - bin/git-cipher
24
+ homepage: https://github.com/wincent/git-cipher
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements:
43
+ - Git
44
+ - GnuPG
45
+ rubyforge_project:
46
+ rubygems_version: 2.0.14
47
+ signing_key:
48
+ specification_version: 4
49
+ summary: Manages encrypted content in a Git repo
50
+ test_files: []