Skip to content

Instantly share code, notes, and snippets.

@funny-falcon
Last active December 29, 2020 10:42
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save funny-falcon/7d6d01e19cc7072e58ee to your computer and use it in GitHub Desktop.
Save funny-falcon/7d6d01e19cc7072e58ee to your computer and use it in GitHub Desktop.
Personal password storage :)
#!/usr/bin/env ruby
require 'digest/sha2'
require 'io/console'
require 'base64'
USAGE = <<EOF
USAGE:
#$0 set domain [file]
- store encoded password for domain
#$0 domain [file]
- feed decoded password for domain to 'xclip -i -selection clipboard'
#$0 -c domain [file]
- feed decoded password for domain to 'xclip -i'
#$0 -o domain [file]
- print decoded password for domain
#$0 -s search [file]
- list matched domains
#$0 -l
- try to decode all passwords with same master password
EOF
unless "".respond_to?(:b)
class String; def b; force_encoding('BINARY'); end; end
end
# Main goal is simplicity of reimplementation:
# custom key deriving and custom cipher to be easy to reimplement in other languages.
# key deriving is not the safest in a world, but should be good enough unless you are James Bond or predisent of America.
# custom cipher function is certainly safe (and slow).
K = 10
M = 1024*1024
def derive(pass)
sha = Digest::SHA512.new
K.times do
all = []
(M/sha.digest_length).times do
all << sha.update(pass).digest
end
sha.update(all.sort.join)
end
sha.update(pass)
sha.digest
end
def encrypt(pass, domain, dpass)
rnd = Random.new
seed = rnd.bytes(8)
sha = Digest::SHA512.new
sha.update(derive(pass+seed+domain))
dpass = "%04x%s%s" % [dpass.bytesize, dpass.b, rnd.bytes(16+rnd.rand(64))]
dpass << sha.dup.update(dpass).digest[0,4]
(dpass.bytesize*2).times do
head = dpass.slice!(0,1).getbyte(0)
dpass << (head ^ sha.dup.update(dpass).digest.getbyte(0))
end
Base64.strict_encode64(seed+dpass)
end
def decrypt(pass, domain, dpass)
dpass = Base64.strict_decode64(dpass)
seed = dpass.slice!(0,8)
sha = Digest::SHA512.new
sha.update(derive(pass+seed+domain))
(dpass.bytesize*2).times do
head = dpass.slice!(-1,1).getbyte(0)
dpass[0,0] = (head ^ sha.dup.update(dpass).digest.getbyte(0)).chr
end
len = dpass[0,4].to_i(16)
unless len <= dpass.bytesize-4 && sha.dup.update(dpass[0...-4]).digest[0,4] == dpass[-4..-1]
return nil
end
dpass[4,len]
end
def decrypt_line(pass, line)
return nil if line.nil? || line.empty?
domain, dpass = line.split("\t", 2)
decrypt(pass, domain, dpass)
end
def query(msg)
if STDIN.tty?
print msg
pass = STDIN.noecho(&:gets).chomp.b
print "\n"
pass
else
pass = STDIN.gets.chomp.b
end
end
def err(m)
$stderr.puts(m)
exit(1)
end
doset = false
dolist = false
dosearch = false
xclip = :board
case ARGV[0]
when 'set'
doset = true
ARGV.shift
when '-c'
xclip = :clip
ARGV.shift
when '-o'
xclip = nil
ARGV.shift
when '-s'
dosearch = true
ARGV.shift
when '-l'
dolist = true
ARGV.shift
end
domen = ARGV.shift unless dolist
if !ARGV.empty?
file = ARGV.shift
elsif ENV["PASSW"]
file = ENV["PASSW"]
else
file = "~/.passw"
end
if !(dolist || !domen.nil?) || !ARGV.empty?
puts USAGE
exit(1)
end
file = File.expand_path(file)
if File.readable?(file)
lines = File.open(file, "rb").readlines.map(&:chomp).select{|l| !l.empty?}
else
lines = []
end
if dosearch
puts lines.map{|l| l.split.first}.grep(Regexp.new(domen))
exit(0)
end
pass = query "Master password:"
if dolist
lines.each do |l|
domen, crypted = l.split("\t",2)
dpass = decrypt(pass, domen, crypted)
if dpass.nil?
printf("%15s !!! fail\n", domen)
else
printf("%15s %s\n", domen, dpass)
end
end
exit(0)
end
d = domen+"\t"
if doset
if domen =~ /^master\d+$/
err("could not overwrite #{domen}") if lines.any?{|l| l.start_with?(d)}
dpass = query "Repeat password:"
err("not matched") unless pass == dpass
else
err("unknown master") unless lines.grep(/^master\d+\s/).any?{|l| decrypt_line(pass, l)}
dpass = query "Domain password:"
dpass2 = query "Repeat password:"
err("not matched") if dpass != dpass2
end
lines.delete_if{|l| l.start_with?(d)}
lines << "#{d}#{encrypt(pass, domen, dpass)}"
File.open(file+".tmp", "wb"){|f| f.write(lines.sort.join("\n")+"\n")}
File.rename(file+".tmp", file)
is_git = File.exist?(File.join(File.dirname(file), ".git"))
if is_git
Dir.chdir(File.dirname(file)) do
system(*%W{git commit -a -m #{Time.now}\ #{domen}})
end
end
else
dpass = decrypt_line(pass, lines.find{|l| l.start_with?(d)})
err("fail") if dpass.nil?
case xclip
when nil
if STDOUT.tty?
puts dpass
else
print dpass
end
when :clip
IO.popen('xclip -i -l 2', 'w'){|f| f.write(dpass)}
Process.daemon
sleep(10)
IO.popen('xclip -i', 'w'){|f| f.write("")}
when :board
IO.popen('xclip -i -l 4 -selection clipboard', 'w'){|f| f.write(dpass)}
Process.daemon
sleep(10)
IO.popen('xclip -i -selection clipboard', 'w'){|f| f.write("")}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment