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.
- checksums.yaml +7 -0
- data/bin/ampk +94 -0
- data/lib/ampk.rb +6 -0
- data/lib/ampk/crypto.rb +54 -0
- data/lib/ampk/reader.rb +232 -0
- data/lib/ampk/version.rb +6 -0
- data/lib/ampk/writer.rb +69 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/ampk
ADDED
@@ -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
|
+
|
data/lib/ampk.rb
ADDED
data/lib/ampk/crypto.rb
ADDED
@@ -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
|
+
|
data/lib/ampk/reader.rb
ADDED
@@ -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
|
+
|
data/lib/ampk/version.rb
ADDED
data/lib/ampk/writer.rb
ADDED
@@ -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: []
|