git-cipher 1.0 → 1.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 +5 -5
- data/bin/git-cipher +102 -60
- data/bin/git-cipher1 +524 -0
- data/bin/git-cipher2 +196 -0
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7d5b65afd443605f7782cec24d66093e353ac155e6b4a1e90aac40da2b8cbfbd
|
4
|
+
data.tar.gz: ab7448447f5ccd3b6467190315e9597e082ae01880ffcdb4cd0e3542b37b0b3e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2dce18072bcee778726a04b35d2103552ba51c860b669428a7ce4c5f9c9c65e49866b6092f1d416b9b88b417a5d1c696be78d8f880b07e87ba71f2d75f0c6e8b
|
7
|
+
data.tar.gz: 91c20158875f8f66ebeb81924a8518f1df9265a1324dd8e9f8bf45ecb6d1787320194229c6e839525cbfc40b4fe622bc46a4e54403cbbb20d5264a8e2fb284fc
|
data/bin/git-cipher
CHANGED
@@ -8,11 +8,12 @@ require 'tempfile'
|
|
8
8
|
|
9
9
|
class Cipher
|
10
10
|
EXTENSION = 'encrypted'
|
11
|
-
|
11
|
+
DEFAULT_GPG_USERS = 'greg@hurrell.net,wincent@github.com'
|
12
12
|
EXECUTABLE_EXTENSIONS = %w[.js .sh]
|
13
13
|
STATUS = {
|
14
|
-
'MISSING' =>
|
15
|
-
'MODIFIED' =>
|
14
|
+
'MISSING' => 0b001,
|
15
|
+
'MODIFIED' => 0b010,
|
16
|
+
'STALE' => 0b100,
|
16
17
|
}
|
17
18
|
VALID_OPTIONS = %w[force help]
|
18
19
|
VALID_SUBCOMMANDS = %w[decrypt encrypt help log ls status]
|
@@ -38,8 +39,7 @@ private
|
|
38
39
|
end
|
39
40
|
|
40
41
|
def check_ignored(path)
|
41
|
-
|
42
|
-
puts "[warning: #{path} is not ignored]" unless $?.exitstatus.zero?
|
42
|
+
puts "[warning: #{path} is not ignored]" unless is_ignored?(path)
|
43
43
|
end
|
44
44
|
|
45
45
|
def colorize(string, color)
|
@@ -59,7 +59,7 @@ private
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def command_path(command)
|
62
|
-
path = `command -v #{escape command}`.chomp
|
62
|
+
path = `sh -c command\\ -v\\ #{escape command}`.chomp
|
63
63
|
die "required dependency #{command} not found" if path.empty?
|
64
64
|
path
|
65
65
|
end
|
@@ -152,17 +152,21 @@ private
|
|
152
152
|
if FileUtils.uptodate?(outfile, [file]) && !@force
|
153
153
|
puts blue('[up to date]')
|
154
154
|
else
|
155
|
+
recipients = gpg_users.
|
156
|
+
split(/\s*,\s*/).
|
157
|
+
map { |u| "--recipient #{escape u}"}.
|
158
|
+
join(' ')
|
155
159
|
print green('[encrypting ...')
|
156
160
|
execute(%{
|
157
161
|
#{escape command_path('gpg')}
|
158
|
-
|
159
|
-
|
162
|
+
--armor
|
163
|
+
--quiet
|
160
164
|
--batch
|
161
165
|
--no-tty
|
162
166
|
--yes
|
163
|
-
|
164
|
-
|
165
|
-
|
167
|
+
#{recipients}
|
168
|
+
--output #{escape outfile}
|
169
|
+
--encrypt #{escape file}
|
166
170
|
})
|
167
171
|
if $?.success?
|
168
172
|
puts green(' done]')
|
@@ -191,16 +195,6 @@ private
|
|
191
195
|
value
|
192
196
|
end
|
193
197
|
|
194
|
-
def get_passphrase
|
195
|
-
stty = escape(command_path('stty'))
|
196
|
-
print 'Passphrase [will not be echoed]: '
|
197
|
-
`#{stty} -echo` # quick hack, cheaper than depending on highline gem
|
198
|
-
STDIN.gets.chomp
|
199
|
-
ensure
|
200
|
-
`#{stty} echo`
|
201
|
-
puts
|
202
|
-
end
|
203
|
-
|
204
198
|
def gpg_decrypt(file, outfile)
|
205
199
|
execute(%{
|
206
200
|
#{escape command_path('gpg')}
|
@@ -214,14 +208,19 @@ private
|
|
214
208
|
})
|
215
209
|
end
|
216
210
|
|
217
|
-
def
|
218
|
-
ENV['GPG_USER'] || get_config('gpguser') ||
|
211
|
+
def gpg_users
|
212
|
+
ENV['GPG_USER'] || get_config('gpguser') || DEFAULT_GPG_USERS
|
219
213
|
end
|
220
214
|
|
221
215
|
def green(string)
|
222
216
|
colorize(string, 32)
|
223
217
|
end
|
224
218
|
|
219
|
+
def is_ignored?(path)
|
220
|
+
`#{escape command_path('git')} check-ignore -q #{escape path}`
|
221
|
+
return $?.exitstatus.zero?
|
222
|
+
end
|
223
|
+
|
225
224
|
def kill_line
|
226
225
|
# 2K deletes the line, 0G moves to column 0
|
227
226
|
# see: http://en.wikipedia.org/wiki/ANSI_escape_code
|
@@ -230,37 +229,36 @@ private
|
|
230
229
|
|
231
230
|
def log
|
232
231
|
if @files.empty?
|
233
|
-
# TODO: would be nice to interleave these instead of doing them serially.
|
234
232
|
puts 'No explicit paths supplied: logging all matching files'
|
235
|
-
|
233
|
+
log!(nil)
|
236
234
|
else
|
237
|
-
|
235
|
+
log!(@files)
|
238
236
|
end
|
239
237
|
end
|
240
238
|
|
241
|
-
def log!(
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
239
|
+
def log!(files)
|
240
|
+
if files.nil?
|
241
|
+
commits = execute(%{
|
242
|
+
#{escape command_path('git')} log
|
243
|
+
--pretty=format:%H
|
244
|
+
--topo-order
|
245
|
+
-- **/.*.#{EXTENSION}
|
246
|
+
}).split
|
247
|
+
else
|
248
|
+
# Would use `--follow` here, but that only works with a single file and, more
|
249
|
+
# importantly, all encrypted files look like random noise, so `--follow`
|
250
|
+
# won't do anything useful; log will stop at first rename.
|
251
|
+
commits = execute(%{
|
252
|
+
#{escape command_path('git')} log
|
253
|
+
--pretty=format:%H
|
254
|
+
--topo-order
|
255
|
+
-- #{files.map { |f| escape(f) }.join(' ')}
|
256
|
+
}).split
|
257
|
+
end
|
247
258
|
|
248
259
|
commits.each do |commit|
|
249
|
-
|
260
|
+
tempfiles = []
|
250
261
|
begin
|
251
|
-
# Get plaintext "post" image.
|
252
|
-
files.push(post = temp_write(show(file, commit)))
|
253
|
-
files.push(
|
254
|
-
post_plaintext = temp_write(gpg_decrypt(post.path, '-'), suffix)
|
255
|
-
)
|
256
|
-
|
257
|
-
# Get plaintext "pre" image.
|
258
|
-
files.push(pre = temp_write(show(file, "#{commit}~")))
|
259
|
-
files.push(pre_plaintext = temp_write(
|
260
|
-
pre.size.zero? ? '' : gpg_decrypt(pre.path, '-'),
|
261
|
-
suffix
|
262
|
-
))
|
263
|
-
|
264
262
|
# Print commit message.
|
265
263
|
puts execute(%{
|
266
264
|
#{escape command_path('git')} --no-pager log
|
@@ -268,17 +266,37 @@ private
|
|
268
266
|
})
|
269
267
|
puts
|
270
268
|
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
269
|
+
# See which files changed in this commit.
|
270
|
+
changed = wc(commit, files).split("\0")
|
271
|
+
|
272
|
+
changed.each do |file|
|
273
|
+
suffix = "-#{File.basename(file)}"
|
274
|
+
|
275
|
+
# Get plaintext "post" image.
|
276
|
+
tempfiles.push(post = temp_write(show(file, commit)))
|
277
|
+
tempfiles.push(post_plaintext = temp_write(
|
278
|
+
post.size.zero? ? '' : gpg_decrypt(post.path, '-'),
|
279
|
+
suffix
|
280
|
+
))
|
281
|
+
|
282
|
+
# Get plaintext "pre" image.
|
283
|
+
tempfiles.push(pre = temp_write(show(file, "#{commit}~")))
|
284
|
+
tempfiles.push(pre_plaintext = temp_write(
|
285
|
+
pre.size.zero? ? '' : gpg_decrypt(pre.path, '-'),
|
286
|
+
suffix
|
287
|
+
))
|
288
|
+
|
289
|
+
# Print pre-to-post diff.
|
290
|
+
puts execute(%{
|
291
|
+
#{escape command_path('git')} --no-pager diff
|
292
|
+
--color=always
|
293
|
+
#{escape pre_plaintext.path}
|
294
|
+
#{escape post_plaintext.path}
|
295
|
+
})
|
296
|
+
end
|
278
297
|
puts
|
279
|
-
|
280
298
|
ensure
|
281
|
-
|
299
|
+
tempfiles.each do |tempfile|
|
282
300
|
tempfile.close
|
283
301
|
tempfile.unlink
|
284
302
|
end
|
@@ -304,7 +322,13 @@ private
|
|
304
322
|
description = yellow('[MODIFIED]')
|
305
323
|
exitstatus |= STATUS['MODIFIED']
|
306
324
|
else
|
307
|
-
|
325
|
+
if (File.mtime(file) - File.mtime(outfile)) > 5
|
326
|
+
# Plain-text is signficantly older than ciphertext.
|
327
|
+
description = yellow('[STALE]')
|
328
|
+
exitstatus |= STATUS['STALE']
|
329
|
+
else
|
330
|
+
description = green('[OK]')
|
331
|
+
end
|
308
332
|
end
|
309
333
|
else
|
310
334
|
description = red('[MISSING]')
|
@@ -316,7 +340,9 @@ private
|
|
316
340
|
end
|
317
341
|
|
318
342
|
def matching
|
319
|
-
Dir.glob("**/*.#{EXTENSION}", File::FNM_DOTMATCH)
|
343
|
+
Dir.glob("**/*.#{EXTENSION}", File::FNM_DOTMATCH).reject do |candidate|
|
344
|
+
is_ignored?(candidate)
|
345
|
+
end
|
320
346
|
end
|
321
347
|
|
322
348
|
# Determine the appropriate mode for the given decrypted plaintext
|
@@ -455,9 +481,10 @@ private
|
|
455
481
|
Shows the status of encrypted files in the current directory and
|
456
482
|
its subdirectories.
|
457
483
|
|
458
|
-
Exits with status #{STATUS['MISSING']} if any decrypted file is missing.
|
459
|
-
Exits with status #{STATUS['MODIFIED']} if any decrypted file
|
460
|
-
Exits with status #{STATUS['
|
484
|
+
Exits with status #{STATUS['MISSING']} if any decrypted file is missing (eg. "MISSING").
|
485
|
+
Exits with status #{STATUS['MODIFIED']} if any decrypted file has modifications (eg. "MODIFIED").
|
486
|
+
Exits with status #{STATUS['STALE']} if any encrypted file has modifications (eg. "STALE").
|
487
|
+
When multiple conditions apply, they are OR-ed together to produce a status code.
|
461
488
|
USAGE
|
462
489
|
else
|
463
490
|
puts strip_heredoc(<<-USAGE)
|
@@ -474,6 +501,21 @@ private
|
|
474
501
|
exit
|
475
502
|
end
|
476
503
|
|
504
|
+
# Show which files (out of `files`) changed in `commit`.
|
505
|
+
def wc(commit, files)
|
506
|
+
if files.nil?
|
507
|
+
execute(%{
|
508
|
+
#{escape command_path('git')} show --name-only --pretty=format: -z #{commit} --
|
509
|
+
**/.*.#{EXTENSION}
|
510
|
+
})
|
511
|
+
else
|
512
|
+
execute(%{
|
513
|
+
#{escape command_path('git')} show --name-only --pretty=format: -z #{commit} --
|
514
|
+
#{files.map { |f| escape(f) }.join(' ')}
|
515
|
+
})
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
477
519
|
def yellow(string)
|
478
520
|
colorize(string, 33)
|
479
521
|
end
|
data/bin/git-cipher1
ADDED
@@ -0,0 +1,524 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# git-cipher -- encrypt/decrypt files
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'pathname'
|
6
|
+
require 'shellwords'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
class Cipher
|
10
|
+
EXTENSION = 'encrypted'
|
11
|
+
DEFAULT_GPG_USERS = 'greg@hurrell.net,wincent@github.com'
|
12
|
+
EXECUTABLE_EXTENSIONS = %w[.js .sh]
|
13
|
+
STATUS = {
|
14
|
+
'MISSING' => 0b001,
|
15
|
+
'MODIFIED' => 0b010,
|
16
|
+
'STALE' => 0b100,
|
17
|
+
}
|
18
|
+
VALID_OPTIONS = %w[force help]
|
19
|
+
VALID_SUBCOMMANDS = %w[decrypt encrypt help log ls status]
|
20
|
+
|
21
|
+
def run
|
22
|
+
send @subcommand
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def initialize
|
28
|
+
@subcommand, @options, @files = process_args
|
29
|
+
|
30
|
+
if @options.include?('help') || @subcommand == 'help'
|
31
|
+
usage(@subcommand)
|
32
|
+
end
|
33
|
+
|
34
|
+
@force = @options.include?('force')
|
35
|
+
end
|
36
|
+
|
37
|
+
def blue(string)
|
38
|
+
colorize(string, 34)
|
39
|
+
end
|
40
|
+
|
41
|
+
def check_ignored(path)
|
42
|
+
puts "[warning: #{path} is not ignored]" unless is_ignored?(path)
|
43
|
+
end
|
44
|
+
|
45
|
+
def colorize(string, color)
|
46
|
+
"\e[#{color}m#{string}\e[0m"
|
47
|
+
end
|
48
|
+
|
49
|
+
def command_name
|
50
|
+
@command_name ||= begin
|
51
|
+
if `ps -p #{Process.ppid.to_i}` =~ /\bgit cipher\b/
|
52
|
+
'git cipher'
|
53
|
+
else
|
54
|
+
File.basename(__FILE__)
|
55
|
+
end
|
56
|
+
rescue
|
57
|
+
File.basename(__FILE__)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def command_path(command)
|
62
|
+
path = `sh -c command\\ -v\\ #{escape command}`.chomp
|
63
|
+
die "required dependency #{command} not found" if path.empty?
|
64
|
+
path
|
65
|
+
end
|
66
|
+
|
67
|
+
def decrypt
|
68
|
+
if @files.empty?
|
69
|
+
puts 'No explicit paths supplied: decrypting all matching files'
|
70
|
+
matching.each { |file| decrypt!(file) }
|
71
|
+
else
|
72
|
+
@files.each { |file| decrypt!(file) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def decrypt!(file)
|
77
|
+
pathname = Pathname.new(file)
|
78
|
+
basename = pathname.basename.to_s
|
79
|
+
unless basename.start_with?('.')
|
80
|
+
die "#{file} does not begin with a period"
|
81
|
+
end
|
82
|
+
|
83
|
+
unless basename.end_with?(".#{EXTENSION}")
|
84
|
+
die "#{file} does not have an .#{EXTENSION} extension"
|
85
|
+
end
|
86
|
+
|
87
|
+
unless pathname.exist?
|
88
|
+
die "#{file} does not exist"
|
89
|
+
end
|
90
|
+
|
91
|
+
outfile = pathname.dirname + basename.gsub(
|
92
|
+
/\A\.|\.#{EXTENSION}\z/, ''
|
93
|
+
)
|
94
|
+
|
95
|
+
print "#{file} -> #{outfile} "
|
96
|
+
|
97
|
+
if FileUtils.uptodate?(outfile, [file]) && !@force
|
98
|
+
# decrypted is newer than encrypted; it might have local changes which we
|
99
|
+
# could blow away, so warn
|
100
|
+
puts red('[warning: plain-text newer than ciphertext; skipping]')
|
101
|
+
else
|
102
|
+
print green('[decrypting ...')
|
103
|
+
gpg_decrypt(file, outfile)
|
104
|
+
if $?.success?
|
105
|
+
puts green(' done]')
|
106
|
+
|
107
|
+
File.chmod(mode(outfile), outfile)
|
108
|
+
|
109
|
+
# Mark plain-text as older than ciphertext, this will prevent a
|
110
|
+
# bin/encrypt run from needlessly changing the contents of the ciphertext
|
111
|
+
# (as the encryption is non-deterministic).
|
112
|
+
time = File.mtime(file) - 1
|
113
|
+
File.utime(time, time, outfile)
|
114
|
+
else
|
115
|
+
print kill_line
|
116
|
+
puts red('[decrypting ... failed; bailing]')
|
117
|
+
exit $?.exitstatus
|
118
|
+
end
|
119
|
+
|
120
|
+
check_ignored(outfile)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def die(msg)
|
125
|
+
STDERR.puts red('error:'), strip_heredoc(msg)
|
126
|
+
exit 1
|
127
|
+
end
|
128
|
+
|
129
|
+
def encrypt
|
130
|
+
if @files.empty?
|
131
|
+
puts 'No explicit paths supplied: encrypting all matching files'
|
132
|
+
matching.each do |file|
|
133
|
+
file = Pathname.new(file)
|
134
|
+
encrypt!(
|
135
|
+
file.dirname +
|
136
|
+
file.basename.to_s.gsub(/\A\.|\.#{EXTENSION}\z/, '')
|
137
|
+
)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
@files.each { |file| encrypt!(Pathname.new(file)) }
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def encrypt!(file)
|
145
|
+
unless file.exist?
|
146
|
+
die "#{file} does not exist"
|
147
|
+
end
|
148
|
+
|
149
|
+
outfile = file.dirname + ".#{file.basename}.#{EXTENSION}"
|
150
|
+
|
151
|
+
print "#{file} -> #{outfile} "
|
152
|
+
if FileUtils.uptodate?(outfile, [file]) && !@force
|
153
|
+
puts blue('[up to date]')
|
154
|
+
else
|
155
|
+
recipients = gpg_users.
|
156
|
+
split(/\s*,\s*/).
|
157
|
+
map { |u| "--recipient #{escape u}"}.
|
158
|
+
join(' ')
|
159
|
+
print green('[encrypting ...')
|
160
|
+
execute(%{
|
161
|
+
#{escape command_path('gpg')}
|
162
|
+
--armor
|
163
|
+
--quiet
|
164
|
+
--batch
|
165
|
+
--no-tty
|
166
|
+
--yes
|
167
|
+
#{recipients}
|
168
|
+
--output #{escape outfile}
|
169
|
+
--encrypt #{escape file}
|
170
|
+
})
|
171
|
+
if $?.success?
|
172
|
+
puts green(' done]')
|
173
|
+
else
|
174
|
+
print kill_line
|
175
|
+
puts red('[encrypting ... failed; bailing]')
|
176
|
+
exit $?.exitstatus
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
File.chmod(mode(file), file)
|
181
|
+
check_ignored(file)
|
182
|
+
end
|
183
|
+
|
184
|
+
def escape(string)
|
185
|
+
Shellwords.escape(string)
|
186
|
+
end
|
187
|
+
|
188
|
+
def execute(string)
|
189
|
+
%x{#{string.gsub("\n", ' ')}}
|
190
|
+
end
|
191
|
+
|
192
|
+
def get_config(key)
|
193
|
+
value = `#{escape command_path('git')} config cipher.#{key}`.chomp
|
194
|
+
return if value.empty?
|
195
|
+
value
|
196
|
+
end
|
197
|
+
|
198
|
+
def gpg_decrypt(file, outfile)
|
199
|
+
execute(%{
|
200
|
+
#{escape command_path('gpg')}
|
201
|
+
-q
|
202
|
+
--yes
|
203
|
+
--batch
|
204
|
+
--no-tty
|
205
|
+
--use-agent
|
206
|
+
-o #{escape outfile}
|
207
|
+
-d #{escape file}
|
208
|
+
})
|
209
|
+
end
|
210
|
+
|
211
|
+
def gpg_users
|
212
|
+
ENV['GPG_USER'] || get_config('gpguser') || DEFAULT_GPG_USERS
|
213
|
+
end
|
214
|
+
|
215
|
+
def green(string)
|
216
|
+
colorize(string, 32)
|
217
|
+
end
|
218
|
+
|
219
|
+
def is_ignored?(path)
|
220
|
+
`#{escape command_path('git')} check-ignore -q #{escape path}`
|
221
|
+
return $?.exitstatus.zero?
|
222
|
+
end
|
223
|
+
|
224
|
+
def kill_line
|
225
|
+
# 2K deletes the line, 0G moves to column 0
|
226
|
+
# see: http://en.wikipedia.org/wiki/ANSI_escape_code
|
227
|
+
"\e[2K\e[0G"
|
228
|
+
end
|
229
|
+
|
230
|
+
def log
|
231
|
+
if @files.empty?
|
232
|
+
puts 'No explicit paths supplied: logging all matching files'
|
233
|
+
log!(nil)
|
234
|
+
else
|
235
|
+
log!(@files)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def log!(files)
|
240
|
+
if files.nil?
|
241
|
+
commits = execute(%{
|
242
|
+
#{escape command_path('git')} log
|
243
|
+
--pretty=format:%H
|
244
|
+
--topo-order
|
245
|
+
-- **/.*.#{EXTENSION}
|
246
|
+
}).split
|
247
|
+
else
|
248
|
+
# Would use `--follow` here, but that only works with a single file and, more
|
249
|
+
# importantly, all encrypted files look like random noise, so `--follow`
|
250
|
+
# won't do anything useful; log will stop at first rename.
|
251
|
+
commits = execute(%{
|
252
|
+
#{escape command_path('git')} log
|
253
|
+
--pretty=format:%H
|
254
|
+
--topo-order
|
255
|
+
-- #{files.map { |f| escape(f) }.join(' ')}
|
256
|
+
}).split
|
257
|
+
end
|
258
|
+
|
259
|
+
commits.each do |commit|
|
260
|
+
tempfiles = []
|
261
|
+
begin
|
262
|
+
# Print commit message.
|
263
|
+
puts execute(%{
|
264
|
+
#{escape command_path('git')} --no-pager log
|
265
|
+
--color=always -1 #{commit}
|
266
|
+
})
|
267
|
+
puts
|
268
|
+
|
269
|
+
# See which files changed in this commit.
|
270
|
+
changed = wc(commit, files).split("\0")
|
271
|
+
|
272
|
+
changed.each do |file|
|
273
|
+
suffix = "-#{File.basename(file)}"
|
274
|
+
|
275
|
+
# Get plaintext "post" image.
|
276
|
+
tempfiles.push(post = temp_write(show(file, commit)))
|
277
|
+
tempfiles.push(post_plaintext = temp_write(
|
278
|
+
post.size.zero? ? '' : gpg_decrypt(post.path, '-'),
|
279
|
+
suffix
|
280
|
+
))
|
281
|
+
|
282
|
+
# Get plaintext "pre" image.
|
283
|
+
tempfiles.push(pre = temp_write(show(file, "#{commit}~")))
|
284
|
+
tempfiles.push(pre_plaintext = temp_write(
|
285
|
+
pre.size.zero? ? '' : gpg_decrypt(pre.path, '-'),
|
286
|
+
suffix
|
287
|
+
))
|
288
|
+
|
289
|
+
# Print pre-to-post diff.
|
290
|
+
puts execute(%{
|
291
|
+
#{escape command_path('git')} --no-pager diff
|
292
|
+
--color=always
|
293
|
+
#{escape pre_plaintext.path}
|
294
|
+
#{escape post_plaintext.path}
|
295
|
+
})
|
296
|
+
end
|
297
|
+
puts
|
298
|
+
ensure
|
299
|
+
tempfiles.each do |tempfile|
|
300
|
+
tempfile.close
|
301
|
+
tempfile.unlink
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def ls
|
308
|
+
matching.each { |file| puts file }
|
309
|
+
end
|
310
|
+
|
311
|
+
def status
|
312
|
+
exitstatus = 0
|
313
|
+
matching.each do |file|
|
314
|
+
pathname = Pathname.new(file)
|
315
|
+
basename = pathname.basename.to_s
|
316
|
+
outfile = pathname.dirname + basename.gsub(
|
317
|
+
/\A\.|\.#{EXTENSION}\z/, ''
|
318
|
+
)
|
319
|
+
if outfile.exist?
|
320
|
+
if FileUtils.uptodate?(outfile, [file])
|
321
|
+
# Plain-text is newer than ciphertext.
|
322
|
+
description = yellow('[MODIFIED]')
|
323
|
+
exitstatus |= STATUS['MODIFIED']
|
324
|
+
else
|
325
|
+
if (File.mtime(file) - File.mtime(outfile)) > 5
|
326
|
+
# Plain-text is signficantly older than ciphertext.
|
327
|
+
description = yellow('[STALE]')
|
328
|
+
exitstatus |= STATUS['STALE']
|
329
|
+
else
|
330
|
+
description = green('[OK]')
|
331
|
+
end
|
332
|
+
end
|
333
|
+
else
|
334
|
+
description = red('[MISSING]')
|
335
|
+
exitstatus |= STATUS['MISSING']
|
336
|
+
end
|
337
|
+
puts "#{file}: #{description}"
|
338
|
+
end
|
339
|
+
exit exitstatus
|
340
|
+
end
|
341
|
+
|
342
|
+
def matching
|
343
|
+
Dir.glob("**/*.#{EXTENSION}", File::FNM_DOTMATCH).reject do |candidate|
|
344
|
+
is_ignored?(candidate)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Determine the appropriate mode for the given decrypted plaintext
|
349
|
+
# `file` based on its file extension.
|
350
|
+
def mode(file)
|
351
|
+
if EXECUTABLE_EXTENSIONS.include?(Pathname.new(file).extname)
|
352
|
+
0700
|
353
|
+
else
|
354
|
+
0600
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def normalize_option(option)
|
359
|
+
normal = option.dup
|
360
|
+
|
361
|
+
if normal.sub!(/\A--/, '') # long option
|
362
|
+
found = VALID_OPTIONS.find { |o| o == normal }
|
363
|
+
elsif normal.sub!(/\A-/, '') # short option
|
364
|
+
found = VALID_OPTIONS.find { |o| o[0] == normal }
|
365
|
+
end
|
366
|
+
|
367
|
+
die "unrecognized option: #{option}" if found.nil?
|
368
|
+
|
369
|
+
found
|
370
|
+
end
|
371
|
+
|
372
|
+
def process_args
|
373
|
+
options, files = ARGV.partition { |arg| arg.start_with?('-') }
|
374
|
+
subcommand = files.shift
|
375
|
+
|
376
|
+
options.map! { |option| normalize_option(option) }
|
377
|
+
|
378
|
+
unless VALID_SUBCOMMANDS.include?(subcommand)
|
379
|
+
if subcommand.nil?
|
380
|
+
message = 'no subcommand'
|
381
|
+
else
|
382
|
+
message = 'unrecognized subcommand'
|
383
|
+
end
|
384
|
+
die [message, "expected one of #{VALID_SUBCOMMANDS.inspect}"].join(': ')
|
385
|
+
end
|
386
|
+
|
387
|
+
[subcommand, options, files]
|
388
|
+
end
|
389
|
+
|
390
|
+
def red(string)
|
391
|
+
colorize(string, 31)
|
392
|
+
end
|
393
|
+
|
394
|
+
def show(file, commit)
|
395
|
+
# Redirect stderr to /dev/null because the file might not have existed prior
|
396
|
+
# to this commit.
|
397
|
+
execute(%{
|
398
|
+
#{escape command_path('git')} show
|
399
|
+
#{commit}:#{escape file} 2> /dev/null
|
400
|
+
})
|
401
|
+
end
|
402
|
+
|
403
|
+
def strip_heredoc(doc)
|
404
|
+
# based on method of same name from Rails
|
405
|
+
indent = doc.scan(/^[ \t]*(?=\S)/).map(&:size).min || 0
|
406
|
+
doc.gsub(/^[ \t]{#{indent}}/, '')
|
407
|
+
end
|
408
|
+
|
409
|
+
def temp_write(contents, suffix = '')
|
410
|
+
file = Tempfile.new(['git-cipher-', suffix])
|
411
|
+
file.write(contents)
|
412
|
+
file.flush
|
413
|
+
file
|
414
|
+
end
|
415
|
+
|
416
|
+
# Print usage information and exit.
|
417
|
+
def usage(subcommand)
|
418
|
+
case subcommand
|
419
|
+
when 'decrypt'
|
420
|
+
puts strip_heredoc(<<-USAGE)
|
421
|
+
#{command_name} decrypt [-f|--force] [FILES...]
|
422
|
+
|
423
|
+
Decrypts files that have been encrypted for storage in version control
|
424
|
+
|
425
|
+
Decrypt two files, but only if the corresponding plain-text files
|
426
|
+
are missing or older:
|
427
|
+
|
428
|
+
#{command_name} decrypt .foo.encrypted .bar.encrypted
|
429
|
+
|
430
|
+
Decrypt all decryptable files:
|
431
|
+
|
432
|
+
#{command_name} decrypt
|
433
|
+
|
434
|
+
(Re-)decrypt all decryptable files, even those whose corresponding
|
435
|
+
plain-text files are newer:
|
436
|
+
|
437
|
+
#{command_name} decrypt -f
|
438
|
+
#{command_name} decrypt --force # (alternative syntax)
|
439
|
+
USAGE
|
440
|
+
when 'encrypt'
|
441
|
+
puts strip_heredoc(<<-USAGE)
|
442
|
+
#{command_name} encrypt [-f|--force] [FILES...]
|
443
|
+
|
444
|
+
Encrypts files for storage in version control
|
445
|
+
|
446
|
+
Encrypt two files, but only if the corresponding ciphertext files
|
447
|
+
are missing or older:
|
448
|
+
|
449
|
+
#{command_name} encrypt foo bar
|
450
|
+
|
451
|
+
Encrypt all encryptable files:
|
452
|
+
|
453
|
+
#{command_name} encrypt
|
454
|
+
|
455
|
+
(Re-)encrypt all encryptable files, even those whose corresponding
|
456
|
+
ciphertext files are newer:
|
457
|
+
|
458
|
+
#{command_name} encrypt -f
|
459
|
+
#{command_name} encrypt --force # (alternative syntax)
|
460
|
+
USAGE
|
461
|
+
when 'log'
|
462
|
+
puts strip_heredoc(<<-USAGE)
|
463
|
+
#{command_name} log FILE
|
464
|
+
|
465
|
+
Shows the log message and decrypted diff for FILE
|
466
|
+
(analogous to `git log -p -- FILE`).
|
467
|
+
|
468
|
+
#{command_name} log foo
|
469
|
+
USAGE
|
470
|
+
when 'ls'
|
471
|
+
puts strip_heredoc(<<-USAGE)
|
472
|
+
#{command_name} ls
|
473
|
+
|
474
|
+
Lists the encrypted files in the current directory and
|
475
|
+
its subdirectories.
|
476
|
+
USAGE
|
477
|
+
when 'status'
|
478
|
+
puts strip_heredoc(<<-USAGE)
|
479
|
+
#{command_name} status
|
480
|
+
|
481
|
+
Shows the status of encrypted files in the current directory and
|
482
|
+
its subdirectories.
|
483
|
+
|
484
|
+
Exits with status #{STATUS['MISSING']} if any decrypted file is missing (eg. "MISSING").
|
485
|
+
Exits with status #{STATUS['MODIFIED']} if any decrypted file has modifications (eg. "MODIFIED").
|
486
|
+
Exits with status #{STATUS['STALE']} if any encrypted file has modifications (eg. "STALE").
|
487
|
+
When multiple conditions apply, they are OR-ed together to produce a status code.
|
488
|
+
USAGE
|
489
|
+
else
|
490
|
+
puts strip_heredoc(<<-USAGE)
|
491
|
+
Available commands (invoke any with -h or --help for more info):
|
492
|
+
|
493
|
+
#{command_name} decrypt
|
494
|
+
#{command_name} encrypt
|
495
|
+
#{command_name} log
|
496
|
+
#{command_name} ls
|
497
|
+
#{command_name} status
|
498
|
+
USAGE
|
499
|
+
end
|
500
|
+
|
501
|
+
exit
|
502
|
+
end
|
503
|
+
|
504
|
+
# Show which files (out of `files`) changed in `commit`.
|
505
|
+
def wc(commit, files)
|
506
|
+
if files.nil?
|
507
|
+
execute(%{
|
508
|
+
#{escape command_path('git')} show --name-only --pretty=format: -z #{commit} --
|
509
|
+
**/.*.#{EXTENSION}
|
510
|
+
})
|
511
|
+
else
|
512
|
+
execute(%{
|
513
|
+
#{escape command_path('git')} show --name-only --pretty=format: -z #{commit} --
|
514
|
+
#{files.map { |f| escape(f) }.join(' ')}
|
515
|
+
})
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
def yellow(string)
|
520
|
+
colorize(string, 33)
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
Cipher.new.run
|
data/bin/git-cipher2
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# TODO: in readme, note threat model and how this is "rolling your own crypto"
|
4
|
+
# even though you might not think that "using openssl" is "rolling your own
|
5
|
+
# crypto"; therefore this is for protecting LOW VALUE secrets (eg. hostnames in
|
6
|
+
# an ssh config file, not cryptocurrency secret keys)
|
7
|
+
#
|
8
|
+
# TODO: think about how dotfiles work. in absence of key, plaintext file is
|
9
|
+
# absent, so we know not to install it... with clean/smudge filters, file is
|
10
|
+
# always present, so we need another way of deciding whether or not to install
|
11
|
+
# (may need a header line or stash the whole thing in JSON)
|
12
|
+
# given that we're going to do encrypt-then-mac, maybe the trailer can be the
|
13
|
+
# signal? ie. we'll have something like:
|
14
|
+
#
|
15
|
+
# U2FsdGVkX1/wDfrOAAAAAJyY66rhru4B0gIY0axns4oLgCEN2Og73UvGzFfCzs8a
|
16
|
+
# H/NRK/4MhpbXNhvoVg/Is5UgrZqtMKs96UoAOtYtzTJweByMhFjaTpyK9Dtz7ctc
|
17
|
+
# ...
|
18
|
+
# UqEPjxtbf4LeUHofwbIXvVzMNqdWrjvpV1wUkaOcHdeDUwUMzcPmct7ZpjEPcSDW
|
19
|
+
# nPfzqyYDoxyT9waSKbYlBQ==
|
20
|
+
# HMAC-SHA256(/etc/passwd)= 0c967cc4b8b040dc7c4185f03247bfbeaa7403f626f8fd9c88cec0d3de86ce95
|
21
|
+
#
|
22
|
+
# TODO: links
|
23
|
+
# https://stackoverflow.com/questions/16056135/how-to-use-openssl-to-encrypt-decrypt-files/31552829
|
24
|
+
# https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pbkdf2-sha256/3993
|
25
|
+
# https://www.daemonology.net/blog/2009-06-24-encrypt-then-mac.html
|
26
|
+
# > Encrypt-and-MAC: The ciphertext is generated by encrypting the plaintext and then appending a MAC of the plaintext. This is approximately how SSH works.
|
27
|
+
# > MAC-then-encrypt: The ciphertext is generated by appending a MAC to the plaintext and then encrypting everything. This is approximately how SSL works.
|
28
|
+
# > Encrypt-then-MAC: The ciphertext is generated by encrypting the plaintext and then appending a MAC of the encrypted plaintext. This is approximately how IPSEC works.
|
29
|
+
# https://crypto.stackexchange.com/questions/202/should-we-mac-then-encrypt-or-encrypt-then-mac
|
30
|
+
#
|
31
|
+
# TODO: prior art
|
32
|
+
# https://github.com/AGWA/git-crypt
|
33
|
+
# https://github.com/StackExchange/blackbox#how-is-the-encryption-done
|
34
|
+
# https://github.com/elasticdog/transcrypt
|
35
|
+
# https://github.com/sobolevn/git-secret
|
36
|
+
# (by no means an exhaustive list... there are so many of them...)
|
37
|
+
class Cipher
|
38
|
+
VALID_SUBCOMMANDS = %w[
|
39
|
+
add
|
40
|
+
help
|
41
|
+
init
|
42
|
+
list
|
43
|
+
log
|
44
|
+
ls
|
45
|
+
register
|
46
|
+
remove
|
47
|
+
rm
|
48
|
+
status
|
49
|
+
unregister
|
50
|
+
]
|
51
|
+
|
52
|
+
def run
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def initialize
|
58
|
+
@subcommand, @options, @files = process_args
|
59
|
+
end
|
60
|
+
|
61
|
+
def colorize(string, color)
|
62
|
+
"\e[#{color}m#{string}\e[0m"
|
63
|
+
end
|
64
|
+
|
65
|
+
def die(message)
|
66
|
+
STDERR.puts red('error:'), dedent(message)
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO need to do encrypt-then-mac https://stackoverflow.com/a/22958889/2103996
|
71
|
+
def encrypt
|
72
|
+
ENV['ENC_PASS'] = 'encryption pass'
|
73
|
+
# note -S must be hex digits
|
74
|
+
# can do cipher instead of enc -e cipher
|
75
|
+
# eg. `openssl aes-256-cbc` instead of `openssl enc -e -aes-256-cbc`
|
76
|
+
# -pass stdin could also work
|
77
|
+
# openssl enc -e cipher -md sha1 -pass env:ENC_PASS -S $salt -in $infile | xxd -p
|
78
|
+
# openssl enc -e cipher -md sha1 -pass env:ENC_PASS -S $salt -in $infile | openssl base64
|
79
|
+
# openssl enc -e cipher -base64 -md sha1 -pass env:ENC_PASS -S $salt -in $infile
|
80
|
+
#
|
81
|
+
# only concern here is that iv won't be random as it comes from pass?
|
82
|
+
# -P will print out the deets here; this shows that when you call with same
|
83
|
+
# salt and pass, you get same key and same iv
|
84
|
+
# when you alter the pass, you get a different key and iv
|
85
|
+
# when you alter the salt, you get a different key and iv
|
86
|
+
#
|
87
|
+
# $ openssl enc -e -aes-256-cbc -base64 -md sha1 -pass pass:secret -S f00dface -in /etc/passwd -P
|
88
|
+
# salt=F00DFACE00000000
|
89
|
+
# key=2CB12DA63001EA3474724000D66FE00ACE073DE41364108ADFE84095280693B9
|
90
|
+
# iv =A18D6BA58FD1916C40E2AB674F575360
|
91
|
+
# $ openssl enc -e -aes-256-cbc -base64 -md sha1 -pass pass:secret -S f00dface -in /etc/passwd -P
|
92
|
+
# salt=F00DFACE00000000
|
93
|
+
# key=2CB12DA63001EA3474724000D66FE00ACE073DE41364108ADFE84095280693B9
|
94
|
+
# iv =A18D6BA58FD1916C40E2AB674F575360
|
95
|
+
# $ openssl enc -e -aes-256-cbc -base64 -md sha1 -pass pass:secret2 -S f00dface -in /etc/passwd -P
|
96
|
+
# salt=F00DFACE00000000
|
97
|
+
# key=AED814B111F3732FBEA095B1E66F2958D3D92E4C00A3ABBCB7FD260515C44E92
|
98
|
+
# iv =D9D9F867C730925ED0C8CA8252CAF5C2
|
99
|
+
# $ openssl enc -e -aes-256-cbc -base64 -md sha1 -pass pass:secret2 -S f00dfacef -in /etc/passwd -P
|
100
|
+
# salt=F00DFACEF0000000
|
101
|
+
# key=04227E47F87C2E8876714976EA5A69B58CB36CD5404D4F60271411EDD2BD076D
|
102
|
+
# iv =A7F83B902B5CA76A8B04AF5652C3B799
|
103
|
+
#
|
104
|
+
# using stronger (slower) key derivation with -pbkdf2 -iter 20000
|
105
|
+
# openssl enc -e -aes-256-cbc -base64 -pbkdf2 -iter 20000 -pass pass:secret -S f00dface -in /etc/passwd -P
|
106
|
+
# (doesn't work on macOS version of openssl; testing on linux two times in a row we get)
|
107
|
+
# `openssl version` on linux prints: OpenSSL 1.1.1q 5 Jul 2022
|
108
|
+
# `openssl version` on macOS prints: LibreSSL 2.8.3
|
109
|
+
# install openssl on macOS is probably not a good idea: https://stackoverflow.com/questions/22795471/utilizing-pbkdf2-with-openssl-library
|
110
|
+
#
|
111
|
+
# hex string is too short, padding with zero bytes to length
|
112
|
+
# salt=F00DFACE00000000
|
113
|
+
# key=D661D07A0672957F64646190C7BF8E6FAEA13C97830E7C036B6C978CD6C89FC7
|
114
|
+
# iv =82BD1988A8E0F4D3E48C40AB366D4174
|
115
|
+
#
|
116
|
+
# hex string is too short, padding with zero bytes to length
|
117
|
+
# salt=F00DFACE00000000
|
118
|
+
# key=D661D07A0672957F64646190C7BF8E6FAEA13C97830E7C036B6C978CD6C89FC7
|
119
|
+
# iv =82BD1988A8E0F4D3E48C40AB366D4174
|
120
|
+
#
|
121
|
+
# (ie. is consistent across runs at least)
|
122
|
+
#
|
123
|
+
# is using sha1 for key derivation a problem? https://security.stackexchange.com/questions/3959/recommended-of-iterations-when-using-pbkdf2-sha256/3993
|
124
|
+
# probably not, because we're not using a user-supplied password; we're using an extremely high entropy long password
|
125
|
+
#
|
126
|
+
# $ openssl enc -e -aes-256-cbc -base64 -md sha1 -pass pass:extremely_long_secret_that_will_actually_be_total_gibberish_we_can_even_pipe_in_binary_crap_to_std_in -S f00dface -in /etc/passwd -P
|
127
|
+
# salt=F00DFACE00000000
|
128
|
+
# key=191531DB1684CFE9D15FBCC1F9D6E7A384E4EC08CDA22B596D6AF83F32113720
|
129
|
+
# iv =738D9C01A176CF97B90F48466188C5FA
|
130
|
+
#
|
131
|
+
# note we can use -md sha256 and that works on macOS and Linux
|
132
|
+
# cf node: https://stackoverflow.com/questions/12219499/whats-wrong-with-nodejs-crypto-decipher
|
133
|
+
# that says i can avoid the salt being prepended... interesting... -nosalt
|
134
|
+
# it is not documented in the linux openssl manpage (see `man enc` instead), but it works in both places and
|
135
|
+
# produces the same results, byte for byte
|
136
|
+
# macOS manpage says:
|
137
|
+
# > This option should never be used since it makes it possible to perform
|
138
|
+
# > efficient dictionary attacks on the password and to attack stream cipher
|
139
|
+
# > encrypted data.
|
140
|
+
# linux:
|
141
|
+
# > Don't use a salt in the key derivation routines. This option SHOULD NOT
|
142
|
+
# > be used except for test purposes or compatibility with ancient versions of
|
143
|
+
# > OpenSSL.
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
# need something random (salt) as input into cbc mode to make it secure
|
150
|
+
# we can't literally use something like this, because git-cipher **does not use passwords**
|
151
|
+
# we need to use GPG to read password, _then_ blah...
|
152
|
+
#
|
153
|
+
# note that salt string must be hex, 16 digits long (if shorter, it gets
|
154
|
+
# padded, if longer, we get an error); equivalent to 64 bits
|
155
|
+
#
|
156
|
+
# note that salt is derived from filename + password (from GPG) + file contents
|
157
|
+
# which means two files can have same contents and will get different salt
|
158
|
+
# and any time file changes, it gets a new salt
|
159
|
+
def salt
|
160
|
+
# $key = "$filename:$password"
|
161
|
+
# echo hello | openssl dgst -hmac $key -sha256
|
162
|
+
# prints: 8e384ff349a3d90f2c7837b0d76de8f81c6e85b390ed38905f521ae77cb9a29a
|
163
|
+
# or: openssl dgst -hmac $key -sha256 $infile
|
164
|
+
# prints: HMAC-SHA256($infile)= 0c967cc4b8b040dc7c4185f03247bfbeaa7403f626f8fd9c88cec0d3de86ce95
|
165
|
+
end
|
166
|
+
|
167
|
+
def process_args
|
168
|
+
options, files = ARGV.partition { |arg| arg.start_with?('-') }
|
169
|
+
subcommand = files.shift
|
170
|
+
|
171
|
+
unless VALID_SUBCOMMANDS.include?(subcommand)
|
172
|
+
if subcommand.nil?
|
173
|
+
message = 'no subcommand'
|
174
|
+
else
|
175
|
+
message = "unrecognized subcommand #{subcommand.inspect}"
|
176
|
+
end
|
177
|
+
die [message, "expected one of #{VALID_SUBCOMMANDS.inspect}"].join(': ')
|
178
|
+
end
|
179
|
+
|
180
|
+
[subcommand, options, files]
|
181
|
+
end
|
182
|
+
|
183
|
+
def red(string)
|
184
|
+
colorize(string, 31)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Based on the strip_heredoc method in Rails.
|
188
|
+
#
|
189
|
+
# See: https://apidock.com/rails/String/strip_heredoc
|
190
|
+
def dedent(doc)
|
191
|
+
indent = doc.scan(/^[ \t]*(?=\S)/).map(&:size).min || 0
|
192
|
+
doc.gsub(/^[ \t]{#{indent}}/, '')
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
Cipher.new.run
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-cipher
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '1.
|
4
|
+
version: '1.1'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Hurrell
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-08-02 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: "\n Provides a convenient workflow for working with encrypted files
|
14
14
|
in a public\n Git repo. Delegates the underlying work of encryption/decryption
|
@@ -21,11 +21,13 @@ extensions: []
|
|
21
21
|
extra_rdoc_files: []
|
22
22
|
files:
|
23
23
|
- bin/git-cipher
|
24
|
+
- bin/git-cipher1
|
25
|
+
- bin/git-cipher2
|
24
26
|
homepage: https://github.com/wincent/git-cipher
|
25
27
|
licenses:
|
26
28
|
- MIT
|
27
29
|
metadata: {}
|
28
|
-
post_install_message:
|
30
|
+
post_install_message:
|
29
31
|
rdoc_options: []
|
30
32
|
require_paths:
|
31
33
|
- lib
|
@@ -42,9 +44,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
42
44
|
requirements:
|
43
45
|
- Git
|
44
46
|
- GnuPG
|
45
|
-
|
46
|
-
|
47
|
-
signing_key:
|
47
|
+
rubygems_version: 3.3.15
|
48
|
+
signing_key:
|
48
49
|
specification_version: 4
|
49
50
|
summary: Manages encrypted content in a Git repo
|
50
51
|
test_files: []
|