]> err.no Git - pwstore/blob - pws
c188a08ffecd63cc9f2c2cc8352c4c63b4c6b968
[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 :readable, :encrypted
92
93   def initialize(filename)
94     content = File.read(filename)
95     (outtxt, stderrtxt, statustxt) = GnuPG.gpgcall(content, %w{--with-colons --no-default-keyring --secret-keyring=/dev/null --keyring=/dev/null})
96     @encrypted = !(statustxt =~ /\[GNUPG:\] NODATA/)
97     if @encrypted
98       @readers = EncryptedFile.list_readers(statustxt)
99       @readable = EncryptedFile.determine_readable(@readers)
100     end
101   end
102
103   def EncryptedFile.determine_readable(readers)
104     GnuPG.get_my_keys.each do |keyid|
105       return true if readers.include?(keyid)
106     end
107     return false
108   end
109
110   def EncryptedFile.list_readers(statustxt)
111     readers = []
112     statustxt.split("\n").each do |line|
113       m = /^\[GNUPG:\] ENC_TO ([0-9A-F]+)/.match line
114       next unless m
115       readers.push m[1]
116     end
117     return readers
118   end
119 end
120
121 class Ls
122   def help(parser, code=0, io=STDOUT)
123     io.puts "Usage: #{$program_name} ls [<directory> ...]"
124     io.puts parser.summarize
125     io.puts "Lists the contents of the given directory/directories, or the current"
126     io.puts "directory if none is given.  For each file show whether it is PGP-encrypted"
127     io.puts "file, and if yes whether we can read it."
128     exit(code)
129   end
130
131   def ls_dir(dirname)
132     begin
133       dir = Dir.open(dirname)
134     rescue Exception => e
135       STDERR.puts e
136       return
137     end
138     puts "#{dirname}:"
139     Dir.chdir(dirname) do
140       dir.sort.each do |filename|
141         next if (filename =~ /^\./) and not (@all >= 3)
142         stat = File::Stat.new(filename)
143         if stat.symlink?
144           puts "(sym)      #{filename}" if (@all >= 2)
145         elsif stat.directory?
146           puts "(dir)      #{filename}" if (@all >= 2)
147         elsif !stat.file?
148           puts "(other)    #{filename}" if (@all >= 2)
149         else
150           f = EncryptedFile.new(filename)
151           if f.encrypted
152             if f.readable
153               puts "(ok)       #{filename}"
154             else
155               puts "(locked)   #{filename}" if (@all >= 1)
156             end
157           else
158             puts "(file)     #{filename}" if (@all >= 2)
159           end
160         end
161       end
162     end
163   end
164
165   def initialize()
166     @all = 0
167     ARGV.options do |opts|
168       opts.on_tail("-h", "--help" , "Display this help screen") { help(opts) }
169       opts.on_tail("-a", "--all" , "Show all files (use up to 3 times to show even more than all)") { @all = @all+1 }
170       opts.parse!
171     end
172
173     dirs = ARGV
174     dirs.push('.') unless dirs.size > 0
175     dirs.each { |dir| ls_dir(dir) }
176   end
177 end
178
179 case ARGV.shift
180   when 'ls': Ls.new
181   else
182     STDERR.puts "What!?"
183 end
184
185 # vim:set shiftwidth=2:
186 # vim:set et:
187 # vim:set ts=2: