ampk 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 16ec52ea4400c790a5d30a3855ba5822fe8f9eea
4
+ data.tar.gz: 5cddbce6840ae250d4ee25f9546d4f04c1e52d44
5
+ SHA512:
6
+ metadata.gz: 21e2b879cadf32f9b6321b87954442ea1d19a3296320937e573becb677e88943290d8397f72f7014fa8897fb3b812f57b9d417178b5b2fea0aaae11f9c47d826
7
+ data.tar.gz: 2035dcc012105fa13b24b923a3b2d98c867bdb2ddc6aad1742375c3078ff4918e4b6289fed2ca6d1e11c56bdfff89ab73a3bb21bf54ffa979debb7cd6ada2500
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib/', __FILE__)
4
+ $:.unshift lib unless $:.include?(lib)
5
+
6
+ require 'ampk'
7
+ require 'optparse'
8
+ Version = [0,0,1]
9
+ def Version.to_s; "v#{join('.')}"; end
10
+
11
+ options = {
12
+ :verbose => false
13
+ }
14
+
15
+ parser = OptionParser.new do |opts|
16
+ opts.banner = "Usage: ampk [options] <archive> [<files>]"
17
+ opts.on('-v', '--verbose', 'Verbose output') do
18
+ options[:verbose] = true
19
+ end
20
+ opts.on('-V', '--version', 'Display version') do
21
+ options[:mode] = :version
22
+ end
23
+ opts.on('-k', '--key=KEYFILE', 'Keyfile') do |key|
24
+ options[:keyfile] = key
25
+ end
26
+ opts.on('-d', '--digest=DIGEST', 'Digest/Password for keyfile') do |digest|
27
+ options[:digest] = digest
28
+ end
29
+ opts.on('-t', '--test', 'Check archive') do |v|
30
+ options[:mode] = :test
31
+ end
32
+ opts.on('-x', '--extract', 'Extract archive contents') do
33
+ options[:mode] = :extract
34
+ end
35
+ opts.on('-c', '--create', 'Create an archive') do
36
+ options[:mode] = :create
37
+ end
38
+ opts.on('-l', '--list', 'List archive contents') do
39
+ options[:mode] = :list
40
+ end
41
+ end
42
+ parser.program_name = File.basename(__FILE__)
43
+ parser.parse!
44
+
45
+ case options[:mode]
46
+ when :create
47
+ key = Crypto::Key.from_file(options[:keyfile], options[:digest]) if options[:keyfile]
48
+ archive = AmpkReader.new(ARGV[0], key)
49
+ if archive.signed_or_encrypted?
50
+ if key.nil? && !options[:nocrypt]
51
+ raise "Archive requires a valid keyfile to verify"
52
+ end
53
+ end
54
+
55
+ archive.verify!
56
+ when :extract
57
+ when :list
58
+ key = Crypto::Key.from_file(options[:keyfile], options[:digest]) if options[:keyfile]
59
+ archive = AmpkReader.new(ARGV[0], key)
60
+
61
+ bits = [['Type', 'Flags', 'Size', 'Filename']]
62
+
63
+ archive.entities.each do |name|
64
+ info = archive.entity(name)
65
+ flags = info[:filter].to_s
66
+ flags += "S" if info[:signature]
67
+ bits << [info[:type], flags, info[:length], name]
68
+ end
69
+
70
+ if options[:verbose]
71
+ col_size = [0]
72
+ bits[0].each_with_index do |item, index|
73
+ col_size[index] = 0
74
+ bits.each do |row|
75
+ col_size[index] = [col_size[index], row[index].to_s.size].max
76
+ end
77
+ end
78
+ fmt = col_size.inject('') do |fmt, size|
79
+ fmt << " | " unless fmt.empty?
80
+ fmt << "%#{size}s"
81
+ end << "\n"
82
+ bits.each_with_index do |bit, index|
83
+ printf fmt, *bit
84
+ printf fmt, *col_size.map { |i| '-' * i } if index.zero?
85
+ end
86
+ else
87
+ puts archive.entities.join("\n")
88
+ end
89
+ when :version
90
+ puts parser.version.to_s
91
+ else
92
+ puts parser.help
93
+ end
94
+
@@ -0,0 +1,6 @@
1
+ require 'zlib'
2
+ require 'ampk/writer'
3
+ require 'ampk/reader'
4
+ require 'ampk/crypto'
5
+ require 'ampk/version'
6
+
@@ -0,0 +1,54 @@
1
+ require 'fileutils'
2
+ require 'openssl'
3
+ require 'base64'
4
+
5
+ module Crypto
6
+ def self.create_keys(priv = "dsa_key", pub = "#{priv}", bits = 1024)
7
+ private_key = OpenSSL::PKey::RSA.new(bits)
8
+ FileUtils.mkdir_p('private')
9
+ FileUtils.mkdir_p('public')
10
+ File.open("private/"+priv+".private", "w+") { |fp| fp << private_key.to_s }
11
+ File.open("public/"+pub+".public", "w+") { |fp| fp << private_key.public_key.to_s }
12
+ private_key
13
+ end
14
+
15
+ class Key
16
+ attr_accessor :digest_string
17
+ def initialize(data, digest_string)
18
+ @public = (data =~ /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/).nil?
19
+ @prefix = @public ? "public" : "private"
20
+ @key = OpenSSL::PKey::RSA.new(data)
21
+ @digest_string = digest_string
22
+ end
23
+
24
+ def self.from_file(filename, digest_string)
25
+ self.new(File.read(filename), digest_string)
26
+ end
27
+
28
+ def encrypt(text)
29
+ Base64.encode64(@key.send("#{@prefix}_encrypt", text))
30
+ end
31
+
32
+ def decrypt(text)
33
+ @key.send("#{@prefix}_decrypt", Base64.decode64(text))
34
+ end
35
+
36
+ def sign(data)
37
+ Base64.encode64(@key.sign(digest, data))
38
+ end
39
+
40
+ def verify(signature, data)
41
+ @key.verify(digest, Base64.decode64(signature), data)
42
+ end
43
+
44
+ def public?
45
+ @public
46
+ end
47
+
48
+ def digest
49
+ OpenSSL::Digest::MD5.new(@digest_string)
50
+ end
51
+ end
52
+ end
53
+
54
+
@@ -0,0 +1,232 @@
1
+ require 'zlib'
2
+
3
+ class AmpkReader
4
+ attr_reader :items, :path
5
+ def initialize(path, public_key)
6
+ @fp = File.open(@path=path,'rb')
7
+ @public_key = public_key
8
+ end
9
+ def verify!(verify_data=false)
10
+ @fp.rewind
11
+ must('start AMPK') { read4 == 'AMPK' }
12
+ until @fp.eof?
13
+ break if peek4 == 'ENDS'
14
+ name = verify_entry('NAME', true)
15
+ headers = {}
16
+ signature = nil
17
+
18
+ until (header = peek4).eql?('DATA')
19
+ headers[header] ||= 0
20
+ headers[header] += 1
21
+ raise "Duplicate #{header} entry for #{name}" if headers[header] > 1
22
+
23
+ case header
24
+ when 'FILT', 'DLEN', 'TYPE'
25
+ verify_entry(header, false)
26
+ when 'SIGN'
27
+ signature = verify_entry('SIGN', true)
28
+ end
29
+ end
30
+
31
+ if signature
32
+ verify_entry_with_sig('DATA', verify_data, signature)
33
+ else
34
+ verify_entry('DATA', verify_data)
35
+ end
36
+ end
37
+ end
38
+ def entities
39
+ auto_scan
40
+ @items.map { |item| item[:name] }
41
+ end
42
+ def entity_signed?(name)
43
+ entity(name).has_key?(:signature)
44
+ end
45
+ def entity_encrypted?(name)
46
+ item = entity(name)
47
+ item.has_key?(:filter) and item[:filter].include?(?C)
48
+ end
49
+ def entity_compressed?(name)
50
+ item = entity(name)
51
+ item.has_key?(:filter) and item[:filter].include?(?Z)
52
+ end
53
+ def entity_size(name)
54
+ entity(name)[:length]
55
+ end
56
+ alias :entity_length :entity_size
57
+ def entity_type(name)
58
+ entity(name)[:type]
59
+ end
60
+ def entity(name)
61
+ auto_scan
62
+ item = @items.find { |item| item[:name] == name }
63
+ raise Errno::ENOENT, name if item.nil?
64
+ item
65
+ end
66
+ def read_entity(name)
67
+ auto_scan unless @items
68
+ item = @items.find { |item| item[:name] == name }
69
+ raise Errno::ENOENT, name if item.nil?
70
+ read_entity_data(item)
71
+ end
72
+ def close
73
+ @fp.close
74
+ end
75
+
76
+ protected
77
+ def auto_scan
78
+ read_headers unless @items
79
+ nil
80
+ end
81
+
82
+ def read_headers
83
+ @fp.rewind
84
+ must('start AMPK') { read4 == 'AMPK' }
85
+ @items = []
86
+ until @fp.eof?
87
+ break if peek4 == 'ENDS'
88
+ @items << read_entity_header()
89
+ skip_entry('DATA')
90
+ end
91
+ @items
92
+ end
93
+
94
+ def read_entity_header
95
+ start = @fp.tell
96
+ name = read_entry('NAME')
97
+ item = { :name => name, :header => start }
98
+ until (header=peek4).eql?('DATA')
99
+ case header
100
+ when 'SIGN'
101
+ item[:signature] = read_entry(header)
102
+ when 'DLEN'
103
+ item[:length] = read_entry(header).to_i
104
+ when 'FILT'
105
+ item[:filter] = read_entry(header)
106
+ when 'TYPE'
107
+ item[:type] = read_entry(header)
108
+ when 'ENCD'
109
+ item[:encoding] = read_entry(header)
110
+ else
111
+ raise RuntimeError, "Unknown/invalid header #{header}"
112
+ end
113
+ end
114
+ item[:offset] = @fp.tell
115
+ item
116
+ end
117
+
118
+ def read_entity_data(item)
119
+ @fp.seek(item[:offset])
120
+ data = read_entry('DATA')
121
+
122
+ if item[:filter]
123
+ item[:filter].reverse.each_byte do |byte|
124
+ case byte
125
+ when ?Z.ord
126
+ data = Zlib::Inflate.inflate(data)
127
+ when ?C.ord
128
+ data = @public_key.decrypt(data)
129
+ end
130
+ end
131
+ end
132
+
133
+ if item[:signature]
134
+ if @public_key.verify(item[:signature], data)
135
+ return decode(item, data)
136
+ else
137
+ raise RuntimeError, "Invalid data for #{item[:name]}"
138
+ end
139
+ else
140
+ return decode(item, data)
141
+ end
142
+ end
143
+
144
+ def decode(item, data)
145
+ if item[:encoding] && data.respond_to?(:encoding)
146
+ data.force_encoding(item[:encoding])
147
+ else
148
+ data
149
+ end
150
+ end
151
+
152
+ def read_entry(name)
153
+ must("have a #{name} entry") { read4 == name }
154
+ size, data = read_data
155
+ must("have some #{name} data") { size == data.size }
156
+ data
157
+ end
158
+
159
+ def skip_entry(name)
160
+ must("have a #{name} entry'") { read4 == name }
161
+ size, skipped = skip_data
162
+ must("have some #{name} data") { size == skipped }
163
+ nil
164
+ end
165
+
166
+ def verify_entry(name, verify_data)
167
+ must("have a #{name} entry") { read4 == name }
168
+ if verify_data
169
+ size, data = read_data
170
+ must("have some #{name} data") { size == data.size }
171
+ data
172
+ else
173
+ size, skipped = skip_data
174
+ must("have at least as much data left") { skipped == size }
175
+ nil
176
+ end
177
+ end
178
+
179
+ def verify_entry_with_sig(name, verify_data, sig)
180
+ data = verify_entry(name, verify_data)
181
+ if data and sig
182
+ must('be valid, signed data') { @public_key.verify(sig, data) }
183
+ end
184
+ end
185
+
186
+ def must(description,&block)
187
+ raise RuntimeError, "Archive is invalid: It must #{description}" unless yield
188
+ end
189
+
190
+ def read4
191
+ str = @fp.read(4)
192
+ str
193
+ end
194
+
195
+ def peek4
196
+ pos = @fp.tell
197
+ str = @fp.read(4)
198
+ @fp.seek(pos, IO::SEEK_SET)
199
+ str
200
+ end
201
+
202
+ def read_data_size
203
+ size = ""
204
+ while (c = get_byte).nonzero?
205
+ size << c
206
+ end
207
+ size.to_i
208
+ end
209
+
210
+ def get_byte
211
+ if @fp.respond_to?(:getbyte)
212
+ @fp.getbyte
213
+ else
214
+ @fp.getc
215
+ end
216
+ end
217
+
218
+ def read_data
219
+ size = read_data_size
220
+ data = @fp.read(size)
221
+ [size, data]
222
+ end
223
+
224
+ def skip_data
225
+ size = read_data_size
226
+ posn = @fp.tell
227
+ @fp.seek(size, IO::SEEK_CUR)
228
+ skipped = @fp.tell - posn
229
+ [size, skipped]
230
+ end
231
+ end
232
+
@@ -0,0 +1,6 @@
1
+ module Ampk
2
+ VERSION = [0,0,5]
3
+ def VERSION.to_s
4
+ self * "."
5
+ end
6
+ end
@@ -0,0 +1,69 @@
1
+ require 'zlib'
2
+
3
+ class AmpkWriter
4
+ def initialize(path, private_key, &block)
5
+ @fp = File.open(path,'wb')
6
+ write("AMPK")
7
+ @private_key = private_key
8
+ if block_given?
9
+ begin
10
+ yield(self)
11
+ ensure
12
+ close()
13
+ end
14
+ end
15
+ end
16
+ def add_entity(name, data, options = {})
17
+ write_entry("NAME", name)
18
+ write_entry("DLEN", size(data).to_s)
19
+
20
+ if data.respond_to?(:encoding)
21
+ write_entry("ENCD", data.encoding.name)
22
+ data = data.force_encoding("ASCII-8BIT")
23
+ end
24
+
25
+ write_entry("TYPE", options[:type] || 'application/octet-stream')
26
+ if options[:sign] || options[:signature]
27
+ write_entry("SIGN", options[:signature] || @private_key.sign(data))
28
+ end
29
+ filter = ""
30
+ if options[:encrypt] || options[:crypt]
31
+ filter << "C"
32
+ data = @private_key.encrypt(data)
33
+ end
34
+ orig_size = size(data)
35
+ if options[:compress] and size(data) > 256
36
+ filter << "Z"
37
+ data = Zlib::Deflate.deflate(data)
38
+ end
39
+ unless filter.empty?
40
+ write_entry('FILT', filter)
41
+ end
42
+ write_size = size(data)
43
+ write_entry("DATA", data)
44
+ end
45
+
46
+ def close
47
+ write('ENDS')
48
+ @fp.close()
49
+ end
50
+
51
+ protected
52
+ def size(data)
53
+ sz = data.size
54
+ sz = data.bytesize if data.respond_to?(:bytesize)
55
+ sz
56
+ end
57
+ def write(str)
58
+ @fp.write(str)
59
+ end
60
+ def write_data(data)
61
+ write("#{size(data)}\0")
62
+ write(data)
63
+ end
64
+ def write_entry(name, data)
65
+ write(name)
66
+ write_data(data)
67
+ end
68
+ end
69
+
metadata ADDED
@@ -0,0 +1,52 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ampk
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Geoff Youngs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: " AMPK is simple archive format which allows contents to be encrypted
14
+ or signed\n \tusing a public/private key system.\n \t\n Entities (i.e. files) can
15
+ be stored encrypted or signed with a private key.\n"
16
+ email: git@intersect-uk.co.uk
17
+ executables:
18
+ - ampk
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - bin/ampk
23
+ - lib/ampk/writer.rb
24
+ - lib/ampk/reader.rb
25
+ - lib/ampk/crypto.rb
26
+ - lib/ampk/version.rb
27
+ - lib/ampk.rb
28
+ homepage: http://github.com/livelink/ampk
29
+ licenses:
30
+ - MIT
31
+ metadata: {}
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubyforge_project:
48
+ rubygems_version: 2.0.3
49
+ signing_key:
50
+ specification_version: 4
51
+ summary: Simple signed/encrypted archive format library
52
+ test_files: []