class GnuPG
@@my_keys = nil
+ @@my_fprs = nil
+ @@keyid_fpr_mapping = {}
def GnuPG.readwrite3(intxt, infd, stdoutfd, stderrfd, statusfd)
outtxt, stderrtxt, statustxt = ''
end
end
end
+ # This is for my private keys, so we can tell if a file is encrypted to us
def GnuPG.get_my_keys()
init_keys
@@my_keys
end
+ # And this is for my private keys also, so we can tell if we are encrypting to ourselves
def GnuPG.get_my_fprs()
init_keys
@@my_fprs
end
+
+ # This maps public keyids to fingerprints, so we can figure
+ # out if a file that is encrypted to a bunch of keys is
+ # encrypted to the fingerprints it should be encrypted to
+ def GnuPG.get_fpr_from_keyid(keyid)
+ fpr = @@keyid_fpr_mapping[keyid]
+ # this can be null, if we tried to find the fpr but failed to find the key in our keyring
+ unless fpr
+ STDERR.puts "Warning: No key found for keyid #{keyid}"
+ end
+ return fpr
+ end
+ def GnuPG.get_fprs_from_keyids(keyids)
+ learn_fingerprints_from_keyids(keyids)
+ return keyids.collect{ |k| get_fpr_from_keyid(k) or "unknown" }
+ end
+
+ # this is to load the keys we will soon be asking about into
+ # our keyid-fpr-mapping hash
+ def GnuPG.learn_fingerprints_from_keyids(keyids)
+ need_to_learn = keyids.reject{ |k| @@keyid_fpr_mapping.has_key?(k) }
+ if need_to_learn.size > 0
+ # we can't use --fast-list-mode here because GnuPG is broken
+ # and does not show elmo's fingerprint in a call like
+ # gpg --with-colons --fast-list-mode --with-fingerprint --list-key D7C3F131AB2A91F5
+ args = %w{--with-colons --with-fingerprint --list-keys}
+ args.concat need_to_learn
+ (outtxt, stderrtxt, statustxt) = GnuPG.gpgcall('', args, true)
+
+ pub = nil
+ fpr = nil
+ outtxt.split("\n").each do |line|
+ parts = line.split(':')
+ if (parts[0] == "pub")
+ pub = parts[4]
+ elsif (parts[0] == "fpr")
+ fpr = parts[9]
+ @@keyid_fpr_mapping[pub] = fpr
+ elsif (parts[0] == "sub")
+ @@keyid_fpr_mapping[parts[4]] = fpr
+ end
+ end
+ end
+ need_to_learn.reject{ |k| @@keyid_fpr_mapping.has_key?(k) }.each { |k| @@keyid_fpr_mapping[k] = nil }
+ end
end
def read_input(query, default_yes=true)
expand_groups
end
+ def verify(content)
+ begin
+ f = File.open(ENV['HOME']+'/.pws-trusted-users')
+ rescue Exception => e
+ STDERR.puts e
+ exit(1)
+ end
+
+ trusted = []
+ f.readlines.each do |line|
+ line.chomp!
+ next if line =~ /^$/
+ next if line =~ /^#/
+
+ trusted.push line
+ end
+
+ (outtxt, stderrtxt, statustxt, exitstatus) = GnuPG.gpgcall(content, %w{}, true)
+ goodsig = false
+ validsig = nil
+ statustxt.split("\n").each do |line|
+ if m = /^\[GNUPG:\] GOODSIG/.match(line)
+ goodsig = true
+ elsif m = /^\[GNUPG:\] VALIDSIG \S+ \S+ \S+ \S+ \S+ \S+ \S+ \S+ \S+ ([0-9A-F]+)/.match(line)
+ validsig = m[1]
+ end
+ end
+
+ if not goodsig
+ STDERR.puts ".users file is not signed properly"
+ exit(1)
+ end
+
+ if not trusted.include?(validsig)
+ STDERR.puts ".users file is signed by #{validsig} which is not in ~/.pws-trusted-users"
+ exit(1)
+ end
+
+ return outtxt
+ end
+
def parse_file
begin
f = File.open('.users')
exit(1)
end
+ users = f.read
+ f.close
+
+ users = verify(users)
+
@users = {}
@groups = {}
lno = 0
- f.readlines.each do |line|
+ users.split("\n").each do |line|
lno = lno+1
next if line =~ /^$/
next if line =~ /^#/
fprs.push @users[t]
end
end
- return ok, fprs
+ return ok, fprs.uniq
end
end
class EncryptedFile
- attr_reader :accessible, :encrypted, :readable
+ attr_reader :accessible, :encrypted, :readable, :readers
def EncryptedFile.determine_readable(readers)
GnuPG.get_my_keys.each do |keyid|
def initialize(filename, new=false)
@groupconfig = GroupConfig.new
@new = new
+ if @new
+ @readers = []
+ end
@filename = filename
unless FileTest.readable?(filename)
def decrypt
(outtxt, stderrtxt, statustxt, exitstatus) = GnuPG.gpgcall(@encrypted_content, %w{--decrypt})
if !@new and exitstatus != 0
- proceed = read_input("Warning: gpg returned non-zero exit status #{exitstatus} when decrypting #{@filename}. Proceed?")
+ proceed = read_input("Warning: gpg returned non-zero exit status #{exitstatus} when decrypting #{@filename}. Proceed?", false)
exit(0) unless proceed
elsif !@new and outtxt.length == 0
proceed = read_input("Warning: #{@filename} decrypted to an empty file. Proceed?")
args.push "--encrypt"
(outtxt, stderrtxt, statustxt, exitstatus) = GnuPG.gpgcall(content, args)
- if exitstatus != 0
- proceed = read_input("Warning: gpg returned non-zero exit status #{exitstatus} when encrypting #{@filename}. Proceed (or try again)?")
- return false unless proceed
- elsif outtxt.length == 0
- tryagain = read_input("Error: #{@filename} decrypted to an empty file. Edit again (or exit)?")
+ invalid = []
+ statustxt.split("\n").each do |line|
+ m = /^\[GNUPG:\] INV_RECP \S+ ([0-9A-F]+)/.match line
+ next unless m
+ invalid.push m[1]
+ end
+ if invalid.size > 0
+ again = read_input("Warning: the following recipients are invalid: #{invalid.join(", ")}. Try again (or proceed)?")
+ return false if again
+ end
+ if outtxt.length == 0
+ tryagain = read_input("Error: #{@filename} encrypted to an empty file. Edit again (or exit)?")
return false if tryagain
exit(0)
end
+ if exitstatus != 0
+ proceed = read_input("Warning: gpg returned non-zero exit status #{exitstatus} when encrypting #{@filename}. Said:\n#{stderrtxt}\n#{statustxt}\n\nProceed (or try again)?")
+ return false unless proceed
+ end
return true, outtxt
end
- def write_back(content)
+ def determine_encryption_targets(content)
targets = EncryptedFile.targets(content)
if targets.size == 0
tryagain = read_input("Warning: Did not find targets to encrypt to in header. Try again (or exit)?", true)
return false if tryagain
end
- ok, encrypted = encrypt(content, expanded)
+ return true, expanded
+ end
+
+ def write_back(content, targets)
+ ok, encrypted = encrypt(content, targets)
return false unless ok
File.open(@filename,"w").write(encrypted)
end
puts "#{dirname}:"
Dir.chdir(dirname) do
+ unless FileTest.exists?(".users")
+ STDERR.puts "The .users file does not exists here. This is not a password store, is it?"
+ exit(1)
+ end
dir.sort.each do |filename|
next if (filename =~ /^\./) and not (@all >= 3)
stat = File::Stat.new(filename)
exit(1)
end
+ encrypted_to = GnuPG.get_fprs_from_keyids(encrypted_file.readers).sort
+
content = encrypted_file.decrypt
+ original_content = content
while true
oldsize = content.length
tempfile = Tempfile.open('pws')
exit(0) unless proceed
end
- success = encrypted_file.write_back(content)
+ ok, targets = encrypted_file.determine_encryption_targets(content)
+ next unless ok
+
+ if (original_content == content)
+ if (targets.sort == encrypted_to)
+ proceed = read_input("Nothing changed. Re-encrypt anyway?", false)
+ exit(0) unless proceed
+ else
+ STDERR.puts("Info: Content not changed but re-encrypting anyway because the list of keys changed")
+ end
+ end
+
+ success = encrypted_file.write_back(content, targets)
break if success
end
end