]> err.no Git - pwstore/blob - pws
Handle files not readable by unix permissions
[pwstore] / pws
1 #!/usr/bin/ruby
2
3 require 'optparse'
4 require 'thread'
5
6 Thread.abort_on_exception = true
7
8 GNUPG = "/usr/bin/gpg"
9
10 $program_name = File.basename($0, '.*')
11
12
13 class GnuPG
14   @@my_keys = nil
15
16   def GnuPG.readwrite3(intxt, infd, stdoutfd, stderrfd, statusfd)
17     outtxt, stderrtxt, statustxt = ''
18     thread_in = Thread.new {
19       infd.print intxt
20       infd.close
21     }
22     thread_out = Thread.new {
23       outtxt = stdoutfd.read
24       stdoutfd.close
25     }
26     thread_err = Thread.new {
27       errtxt = stderrfd.read
28       stderrfd.close
29     }
30     thread_status = Thread.new {
31       statustxt = statusfd.read
32       statusfd.close
33     } if (statusfd)
34
35     thread_in.join
36     thread_out.join
37     thread_err.join
38     thread_status.join if thread_status
39
40     return outtxt, stderrtxt, statustxt
41   end
42
43   def GnuPG.gpgcall(intxt, args, require_success = false)
44     inR, inW = IO.pipe
45     outR, outW = IO.pipe
46     errR, errW = IO.pipe
47     statR, statW = IO.pipe
48
49     pid = Kernel.fork do
50       inW.close
51       outR.close
52       errR.close
53       statR.close
54       STDIN.reopen(inR)
55       STDOUT.reopen(outW)
56       STDERR.reopen(errW)
57       exec(GNUPG, "--status-fd=#{statW.fileno}",  *args)
58       raise ("Calling gnupg failed")
59     end
60     inR.close
61     outW.close
62     errW.close
63     statW.close
64     (outtxt, stderrtxt, statustxt) = readwrite3(intxt, inW, outR, errR, statR);
65     wpid, status = Process.waitpid2 pid
66     throw "Unexpected pid: #{pid} vs #{wpid}" unless pid == wpid
67     throw "Process has not exited!?" unless status.exited?
68     throw "gpg call did not exit sucessfully" if (require_success and status.exitstatus != 0)
69     return outtxt, stderrtxt, statustxt, status.exitstatus
70   end
71
72   def GnuPG.init_keys()
73     return if @@my_keys
74     (outtxt, stderrtxt, statustxt) = GnuPG.gpgcall('', %w{--fast-list-mode --with-colons --list-secret-keys}, true)
75     @@my_keys = []
76     outtxt.split("\n").each do |line|
77       parts = line.split(':')
78       if (parts[0] == "ssb" or parts[0] == "sec")
79         @@my_keys.push parts[4]
80       end
81     end
82   end
83
84   def GnuPG.get_my_keys()
85     init_keys
86     @@my_keys
87   end
88 end
89
90 class EncryptedFile
91   attr_reader :accessible, :encrypted, :readable
92
93   def initialize(filename)
94     unless FileTest.readable?(filename)
95       @accessible = false
96       return
97     end
98     @accessible = true
99     content = File.read(filename)
100     (outtxt, stderrtxt, statustxt) = GnuPG.gpgcall(content, %w{--with-colons --no-default-keyring --secret-keyring=/dev/null --keyring=/dev/null})
101     @encrypted = !(statustxt =~ /\[GNUPG:\] NODATA/)
102     if @encrypted
103       @readers = EncryptedFile.list_readers(statustxt)
104       @readable = EncryptedFile.determine_readable(@readers)
105     end
106   end
107
108   def EncryptedFile.determine_readable(readers)
109     GnuPG.get_my_keys.each do |keyid|
110       return true if readers.include?(keyid)
111     end
112     return false
113   end
114
115   def EncryptedFile.list_readers(statustxt)
116     readers = []
117     statustxt.split("\n").each do |line|
118       m = /^\[GNUPG:\] ENC_TO ([0-9A-F]+)/.match line
119       next unless m
120       readers.push m[1]
121     end
122     return readers
123   end
124 end
125
126 class Ls
127   def help(parser, code=0, io=STDOUT)
128     io.puts "Usage: #{$program_name} ls [<directory> ...]"
129     io.puts parser.summarize
130     io.puts "Lists the contents of the given directory/directories, or the current"
131     io.puts "directory if none is given.  For each file show whether it is PGP-encrypted"
132     io.puts "file, and if yes whether we can read it."
133     exit(code)
134   end
135
136   def ls_dir(dirname)
137     begin
138       dir = Dir.open(dirname)
139     rescue Exception => e
140       STDERR.puts e
141       return
142     end
143     puts "#{dirname}:"
144     Dir.chdir(dirname) do
145       dir.sort.each do |filename|
146         next if (filename =~ /^\./) and not (@all >= 3)
147         stat = File::Stat.new(filename)
148         if stat.symlink?
149           puts "(sym)      #{filename}" if (@all >= 2)
150         elsif stat.directory?
151           puts "(dir)      #{filename}" if (@all >= 2)
152         elsif !stat.file?
153           puts "(other)    #{filename}" if (@all >= 2)
154         else
155           f = EncryptedFile.new(filename)
156           if !f.accessible
157             puts "(!perm)    #{filename}"
158           elsif !f.encrypted
159             puts "(file)     #{filename}" if (@all >= 2)
160           elsif f.readable
161             puts "(ok)       #{filename}"
162           else
163             puts "(locked)   #{filename}" if (@all >= 1)
164           end
165         end
166       end
167     end
168   end
169
170   def initialize()
171     @all = 0
172     ARGV.options do |opts|
173       opts.on_tail("-h", "--help" , "Display this help screen") { help(opts) }
174       opts.on_tail("-a", "--all" , "Show all files (use up to 3 times to show even more than all)") { @all = @all+1 }
175       opts.parse!
176     end
177
178     dirs = ARGV
179     dirs.push('.') unless dirs.size > 0
180     dirs.each { |dir| ls_dir(dir) }
181   end
182 end
183
184 case ARGV.shift
185   when 'ls': Ls.new
186   else
187     STDERR.puts "What!?"
188 end
189
190 # vim:set shiftwidth=2:
191 # vim:set et:
192 # vim:set ts=2: