git-cipher 0.1
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.
- checksums.yaml +7 -0
- data/bin/git-cipher +380 -0
- 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: []
|