]> err.no Git - pwstore/blobdiff - pws
gnupg is broken. news @11
[pwstore] / pws
diff --git a/pws b/pws
index 7cbc8f4605896dd0e6387067032b2295605671ad..cccb42c2214b7ce913319a9bcbd9c6eea15bebe9 100755 (executable)
--- a/pws
+++ b/pws
@@ -46,6 +46,8 @@ end
 
 class GnuPG
   @@my_keys = nil
+  @@my_fprs = nil
+  @@keyid_fpr_mapping = {}
 
   def GnuPG.readwrite3(intxt, infd, stdoutfd, stderrfd, statusfd)
     outtxt, stderrtxt, statustxt = ''
@@ -117,14 +119,61 @@ class GnuPG
       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)
@@ -153,6 +202,47 @@ class GroupConfig
     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')
@@ -161,11 +251,16 @@ class GroupConfig
       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 =~ /^#/
@@ -261,12 +356,12 @@ class GroupConfig
         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|
@@ -296,6 +391,9 @@ class EncryptedFile
   def initialize(filename, new=false)
     @groupconfig = GroupConfig.new
     @new = new
+    if @new
+      @readers = []
+    end
 
     @filename = filename
     unless FileTest.readable?(filename)
@@ -315,7 +413,7 @@ class EncryptedFile
   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?")
@@ -331,20 +429,31 @@ class EncryptedFile
     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)
@@ -374,7 +483,11 @@ class EncryptedFile
       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)
@@ -401,6 +514,10 @@ class Ls
     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)
@@ -455,7 +572,10 @@ class Ed
       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')
@@ -484,7 +604,19 @@ class Ed
         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