class Cipher
Constants
- DEFAULT_GPG_USER
- EXECUTABLE_EXTENSIONS
- EXTENSION
- STATUS
- VALID_OPTIONS
- VALID_SUBCOMMANDS
Public Class Methods
new()
click to toggle source
# File bin/git-cipher, line 26 def initialize @subcommand, @options, @files = process_args if @options.include?('help') || @subcommand == 'help' usage(@subcommand) end @force = @options.include?('force') end
Public Instance Methods
run()
click to toggle source
# File bin/git-cipher, line 20 def run send @subcommand end
Private Instance Methods
blue(string)
click to toggle source
# File bin/git-cipher, line 36 def blue(string) colorize(string, 34) end
check_ignored(path)
click to toggle source
# File bin/git-cipher, line 40 def check_ignored(path) `#{escape command_path('git')} check-ignore -q #{escape path}` puts "[warning: #{path} is not ignored]" unless $?.exitstatus.zero? end
colorize(string, color)
click to toggle source
# File bin/git-cipher, line 45 def colorize(string, color) "\e[#{color}m#{string}\e[0m" end
command_name()
click to toggle source
# File bin/git-cipher, line 49 def command_name @command_name ||= begin if `ps -p #{Process.ppid.to_i}` =~ /\bgit cipher\b/ 'git cipher' else File.basename(__FILE__) end rescue File.basename(__FILE__) end end
command_path(command)
click to toggle source
# File bin/git-cipher, line 61 def command_path(command) path = `command -v #{escape command}`.chomp die "required dependency #{command} not found" if path.empty? path end
decrypt()
click to toggle source
# File bin/git-cipher, line 67 def decrypt if @files.empty? puts 'No explicit paths supplied: decrypting all matching files' matching.each { |file| decrypt!(file) } else @files.each { |file| decrypt!(file) } end end
decrypt!(file)
click to toggle source
# File bin/git-cipher, line 76 def decrypt!(file) pathname = Pathname.new(file) basename = pathname.basename.to_s unless basename.start_with?('.') die "#{file} does not begin with a period" end unless basename.end_with?(".#{EXTENSION}") die "#{file} does not have an .#{EXTENSION} extension" end unless pathname.exist? die "#{file} does not exist" end outfile = pathname.dirname + basename.gsub( /\A\.|\.#{EXTENSION}\z/, '' ) print "#{file} -> #{outfile} " if FileUtils.uptodate?(outfile, [file]) && !@force # decrypted is newer than encrypted; it might have local changes which we # could blow away, so warn puts red('[warning: plain-text newer than ciphertext; skipping]') else print green('[decrypting ...') gpg_decrypt(file, outfile) if $?.success? puts green(' done]') File.chmod(mode(outfile), outfile) # Mark plain-text as older than ciphertext, this will prevent a # bin/encrypt run from needlessly changing the contents of the ciphertext # (as the encryption is non-deterministic). time = File.mtime(file) - 1 File.utime(time, time, outfile) else print kill_line puts red('[decrypting ... failed; bailing]') exit $?.exitstatus end check_ignored(outfile) end end
die(msg)
click to toggle source
# File bin/git-cipher, line 124 def die(msg) STDERR.puts red('error:'), strip_heredoc(msg) exit 1 end
encrypt()
click to toggle source
# File bin/git-cipher, line 129 def encrypt if @files.empty? puts 'No explicit paths supplied: encrypting all matching files' matching.each do |file| file = Pathname.new(file) encrypt!( file.dirname + file.basename.to_s.gsub(/\A\.|\.#{EXTENSION}\z/, '') ) end else @files.each { |file| encrypt!(Pathname.new(file)) } end end
encrypt!(file)
click to toggle source
# File bin/git-cipher, line 144 def encrypt!(file) unless file.exist? die "#{file} does not exist" end outfile = file.dirname + ".#{file.basename}.#{EXTENSION}" print "#{file} -> #{outfile} " if FileUtils.uptodate?(outfile, [file]) && !@force puts blue('[up to date]') else print green('[encrypting ...') execute(%{ #{escape command_path('gpg')} -a -q --batch --no-tty --yes -r #{escape gpg_user} -o #{escape outfile} -e #{escape file} }) if $?.success? puts green(' done]') else print kill_line puts red('[encrypting ... failed; bailing]') exit $?.exitstatus end end File.chmod(mode(file), file) check_ignored(file) end
escape(string)
click to toggle source
# File bin/git-cipher, line 180 def escape(string) Shellwords.escape(string) end
execute(string)
click to toggle source
# File bin/git-cipher, line 184 def execute(string) %x{#{string.gsub("\n", ' ')}} end
get_config(key)
click to toggle source
# File bin/git-cipher, line 188 def get_config(key) value = `#{escape command_path('git')} config cipher.#{key}`.chomp return if value.empty? value end
get_passphrase()
click to toggle source
# File bin/git-cipher, line 194 def get_passphrase stty = escape(command_path('stty')) print 'Passphrase [will not be echoed]: ' `#{stty} -echo` # quick hack, cheaper than depending on highline gem STDIN.gets.chomp ensure `#{stty} echo` puts end
gpg_decrypt(file, outfile)
click to toggle source
# File bin/git-cipher, line 204 def gpg_decrypt(file, outfile) execute(%{ #{escape command_path('gpg')} -q --yes --batch --no-tty --use-agent -o #{escape outfile} -d #{escape file} }) end
gpg_user()
click to toggle source
# File bin/git-cipher, line 217 def gpg_user ENV['GPG_USER'] || get_config('gpguser') || DEFAULT_GPG_USER end
green(string)
click to toggle source
# File bin/git-cipher, line 221 def green(string) colorize(string, 32) end
kill_line()
click to toggle source
# File bin/git-cipher, line 225 def kill_line # 2K deletes the line, 0G moves to column 0 # see: http://en.wikipedia.org/wiki/ANSI_escape_code "\e[2K\e[0G" end
log()
click to toggle source
# File bin/git-cipher, line 231 def log if @files.empty? # TODO: would be nice to interleave these instead of doing them serially. puts 'No explicit paths supplied: logging all matching files' matching.each { |file| log!(file) } else @files.each { |file| log!(file) } end end
log!(file)
click to toggle source
# File bin/git-cipher, line 241 def log!(file) commits = execute(%{ #{escape command_path('git')} log --pretty=format:%H -- #{escape file} }).split suffix = "-#{File.basename(file)}" commits.each do |commit| files = [] begin # Get plaintext "post" image. files.push(post = temp_write(show(file, commit))) files.push( post_plaintext = temp_write(gpg_decrypt(post.path, '-'), suffix) ) # Get plaintext "pre" image. files.push(pre = temp_write(show(file, "#{commit}~"))) files.push(pre_plaintext = temp_write( pre.size.zero? ? '' : gpg_decrypt(pre.path, '-'), suffix )) # Print commit message. puts execute(%{ #{escape command_path('git')} --no-pager log --color=always -1 #{commit} }) puts # Print pre-to-post diff. puts execute(%{ #{escape command_path('git')} --no-pager diff --color=always #{escape pre_plaintext.path} #{escape post_plaintext.path} }) puts ensure files.each do |tempfile| tempfile.close tempfile.unlink end end end end
ls()
click to toggle source
# File bin/git-cipher, line 289 def ls matching.each { |file| puts file } end
matching()
click to toggle source
# File bin/git-cipher, line 318 def matching Dir.glob("**/*.#{EXTENSION}", File::FNM_DOTMATCH) end
mode(file)
click to toggle source
Determine the appropriate mode for the given decrypted plaintext `file` based on its file extension.
# File bin/git-cipher, line 324 def mode(file) if EXECUTABLE_EXTENSIONS.include?(Pathname.new(file).extname) 0700 else 0600 end end
normalize_option(option)
click to toggle source
# File bin/git-cipher, line 332 def normalize_option(option) normal = option.dup if normal.sub!(/\A--/, '') # long option found = VALID_OPTIONS.find { |o| o == normal } elsif normal.sub!(/\A-/, '') # short option found = VALID_OPTIONS.find { |o| o[0] == normal } end die "unrecognized option: #{option}" if found.nil? found end
process_args()
click to toggle source
# File bin/git-cipher, line 346 def process_args options, files = ARGV.partition { |arg| arg.start_with?('-') } subcommand = files.shift options.map! { |option| normalize_option(option) } unless VALID_SUBCOMMANDS.include?(subcommand) if subcommand.nil? message = 'no subcommand' else message = 'unrecognized subcommand' end die [message, "expected one of #{VALID_SUBCOMMANDS.inspect}"].join(': ') end [subcommand, options, files] end
red(string)
click to toggle source
# File bin/git-cipher, line 364 def red(string) colorize(string, 31) end
show(file, commit)
click to toggle source
# File bin/git-cipher, line 368 def show(file, commit) # Redirect stderr to /dev/null because the file might not have existed prior # to this commit. execute(%{ #{escape command_path('git')} show #{commit}:#{escape file} 2> /dev/null }) end
status()
click to toggle source
# File bin/git-cipher, line 293 def status exitstatus = 0 matching.each do |file| pathname = Pathname.new(file) basename = pathname.basename.to_s outfile = pathname.dirname + basename.gsub( /\A\.|\.#{EXTENSION}\z/, '' ) if outfile.exist? if FileUtils.uptodate?(outfile, [file]) # Plain-text is newer than ciphertext. description = yellow('[MODIFIED]') exitstatus |= STATUS['MODIFIED'] else description = green('[OK]') end else description = red('[MISSING]') exitstatus |= STATUS['MISSING'] end puts "#{file}: #{description}" end exit exitstatus end
strip_heredoc(doc)
click to toggle source
# File bin/git-cipher, line 377 def strip_heredoc(doc) # based on method of same name from Rails indent = doc.scan(/^[ \t]*(?=\S)/).map(&:size).min || 0 doc.gsub(/^[ \t]{#{indent}}/, '') end
temp_write(contents, suffix = '')
click to toggle source
# File bin/git-cipher, line 383 def temp_write(contents, suffix = '') file = Tempfile.new(['git-cipher-', suffix]) file.write(contents) file.flush file end
usage(subcommand)
click to toggle source
Print usage information and exit.
# File bin/git-cipher, line 391 def usage(subcommand) case subcommand when 'decrypt' puts strip_heredoc(<<-USAGE) #{command_name} decrypt [-f|--force] [FILES...] Decrypts files that have been encrypted for storage in version control Decrypt two files, but only if the corresponding plain-text files are missing or older: #{command_name} decrypt .foo.encrypted .bar.encrypted Decrypt all decryptable files: #{command_name} decrypt (Re-)decrypt all decryptable files, even those whose corresponding plain-text files are newer: #{command_name} decrypt -f #{command_name} decrypt --force # (alternative syntax) USAGE when 'encrypt' puts strip_heredoc(<<-USAGE) #{command_name} encrypt [-f|--force] [FILES...] Encrypts files for storage in version control Encrypt two files, but only if the corresponding ciphertext files are missing or older: #{command_name} encrypt foo bar Encrypt all encryptable files: #{command_name} encrypt (Re-)encrypt all encryptable files, even those whose corresponding ciphertext files are newer: #{command_name} encrypt -f #{command_name} encrypt --force # (alternative syntax) USAGE when 'log' puts strip_heredoc(<<-USAGE) #{command_name} log FILE Shows the log message and decrypted diff for FILE (analogous to `git log -p -- FILE`). #{command_name} log foo USAGE when 'ls' puts strip_heredoc(<<-USAGE) #{command_name} ls Lists the encrypted files in the current directory and its subdirectories. USAGE when 'status' puts strip_heredoc(<<-USAGE) #{command_name} status Shows the status of encrypted files in the current directory and its subdirectories. Exits with status #{STATUS['MISSING']} if any decrypted file is missing. Exits with status #{STATUS['MODIFIED']} if any decrypted file is modified. Exits with status #{STATUS['MISSING'] | STATUS['MODIFIED']} if both of the above apply. USAGE else puts strip_heredoc(<<-USAGE) Available commands (invoke any with -h or --help for more info): #{command_name} decrypt #{command_name} encrypt #{command_name} log #{command_name} ls #{command_name} status USAGE end exit end
yellow(string)
click to toggle source
# File bin/git-cipher, line 477 def yellow(string) colorize(string, 33) end