tem_drm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/LICENSE +21 -0
- data/Manifest +16 -0
- data/README +3 -0
- data/bin/drm_key2proc.rb +28 -0
- data/bin/drm_proxy.rb +219 -0
- data/bin/drm_unwrap.rb +48 -0
- data/bin/drm_wrap.rb +22 -0
- data/lib/drm/drm.rb +11 -0
- data/lib/drm/http_procs.rb +17 -0
- data/lib/drm/io_procs.rb +16 -0
- data/lib/drm/metadata.rb +119 -0
- data/lib/drm/tem.rb +37 -0
- data/lib/drm/unwrapper.rb +40 -0
- data/lib/drm/wrapper.rb +40 -0
- data/tem_drm.gemspec +53 -0
- data/test/kick_vlc.xspf +11 -0
- metadata +98 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2007 Massachusetts Institute of Technology
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/Manifest
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
bin/drm_wrap.rb
|
2
|
+
bin/drm_key2proc.rb
|
3
|
+
bin/drm_unwrap.rb
|
4
|
+
bin/drm_proxy.rb
|
5
|
+
LICENSE
|
6
|
+
test/kick_vlc.xspf
|
7
|
+
lib/drm/unwrapper.rb
|
8
|
+
lib/drm/wrapper.rb
|
9
|
+
lib/drm/tem.rb
|
10
|
+
lib/drm/io_procs.rb
|
11
|
+
lib/drm/metadata.rb
|
12
|
+
lib/drm/http_procs.rb
|
13
|
+
lib/drm/drm.rb
|
14
|
+
README
|
15
|
+
CHANGELOG
|
16
|
+
Manifest
|
data/README
ADDED
data/bin/drm_key2proc.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'drm'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
gem 'tem_ruby'
|
6
|
+
require 'tem_ruby'
|
7
|
+
|
8
|
+
unless ARGV.length == 2
|
9
|
+
print "Usage: #{$0} key_file proc_file\n"
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
$terminal = Tem::SCard::JCOPRemoteTerminal.new
|
14
|
+
unless $terminal.connect
|
15
|
+
$terminal.disconnect
|
16
|
+
$terminal = Tem::SCard::PCSCTerminal.new
|
17
|
+
$terminal.connect
|
18
|
+
end
|
19
|
+
$javacard = Tem::SCard::JavaCard.new($terminal)
|
20
|
+
$tem = Tem::Session.new($javacard)
|
21
|
+
tem_pubek = $tem.pubek
|
22
|
+
|
23
|
+
key_fname = ARGV[0]
|
24
|
+
proc_fname = ARGV[1]
|
25
|
+
|
26
|
+
key = File.open(key_fname, 'rb') { |f| f.read }
|
27
|
+
proc = DRM::Tem.tem_proc_for_filekey key, tem_pubek
|
28
|
+
File.open(proc_fname, 'wb') { |f| f.write proc.to_yaml_str }
|
data/bin/drm_proxy.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'socket'
|
3
|
+
require 'thread'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
gem 'tem_ruby'
|
8
|
+
require 'tem_ruby'
|
9
|
+
|
10
|
+
require 'drm.rb'
|
11
|
+
|
12
|
+
class Socket
|
13
|
+
def self.pack_sockaddr_in6(port, address, options = {})
|
14
|
+
raise 'wrong address format' unless address.length == 16
|
15
|
+
[28, Socket::AF_INET6, options[:flowinfo] || 0, port].pack('CCnN') + address.pack('C*') + [0].pack('N')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class DRM::Proxy
|
20
|
+
def initialize(conf)
|
21
|
+
@key_prefix = conf[:key_prefix]
|
22
|
+
|
23
|
+
@tcp4 = Socket.new Socket::AF_INET, Socket::SOCK_STREAM, Socket::PF_UNSPEC
|
24
|
+
@tcp4.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
25
|
+
# @tcp4.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true
|
26
|
+
@tcp4.bind Socket.pack_sockaddr_in(conf[:port], '0.0.0.0')
|
27
|
+
@tcp4.listen 10
|
28
|
+
@tcp4_thread = Thread.new(@tcp4) { |s| sock_loop s }
|
29
|
+
|
30
|
+
begin
|
31
|
+
@tcp6 = Socket.new Socket::AF_INET6, Socket::SOCK_STREAM, Socket::PF_UNSPEC
|
32
|
+
@tcp6.setsockopt Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true
|
33
|
+
# @tcp6.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true
|
34
|
+
@tcp6.bind Socket.pack_sockaddr_in6(conf[:port], Array.new(16, 0))
|
35
|
+
@tcp6.listen 10
|
36
|
+
@tcp6_thread = Thread.new(@tcp6) { |s| sock_loop s }
|
37
|
+
rescue
|
38
|
+
puts "Borg IPv6 setup failed: #{$!}"
|
39
|
+
@tcp6 = nil
|
40
|
+
@tcp6_thread = nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def sock_loop(socket)
|
45
|
+
while true do
|
46
|
+
begin
|
47
|
+
client_sock, client_sockaddr = socket.accept
|
48
|
+
print "Accepted client with address #{client_sockaddr.unpack('C*').join(', ')}\n"
|
49
|
+
Thread.new(client_sock) do |s|
|
50
|
+
begin
|
51
|
+
serve_client s
|
52
|
+
rescue
|
53
|
+
print "Unexpected exception #{$!}\n"
|
54
|
+
print $!.backtrace.join("\n") + "\n"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
rescue
|
58
|
+
print "Unexpected exception #{$!}\n"
|
59
|
+
print $!.backtrace.join("\n") + "\n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def serve_client(socket)
|
65
|
+
headers_string = ''
|
66
|
+
while true do
|
67
|
+
hdr = socket.recv 4096
|
68
|
+
headers_string += hdr
|
69
|
+
break if headers_string =~ /(\r\n){2}/
|
70
|
+
end
|
71
|
+
|
72
|
+
headers_array = headers_string.split("\r\n")
|
73
|
+
request = Hash[*([:method, :uri, :protocol].zip(headers_array.shift.split(' ')).reject { |va| va.any? { |vm| vm.nil? }}.flatten)]
|
74
|
+
headers = Hash[*(headers_array.map { |line| line.split(':', 2).map { |e| e.strip } }.flatten)]
|
75
|
+
|
76
|
+
serve_request request, headers, socket
|
77
|
+
socket.close unless socket.closed?
|
78
|
+
end
|
79
|
+
|
80
|
+
def serve_request(request, headers, socket)
|
81
|
+
pp request
|
82
|
+
pp headers
|
83
|
+
|
84
|
+
raise "I don't know how to do #{request[:method]}" unless request[:method] == 'GET'
|
85
|
+
|
86
|
+
pp request[:uri]
|
87
|
+
match_data = request[:uri].match(/^\/\?url\=(.*)/)
|
88
|
+
unless match_data.nil?
|
89
|
+
real_uri = URI.unescape(match_data[1])
|
90
|
+
drm_uri = URI.escape(real_uri + '.drm-enc')
|
91
|
+
pp [real_uri, drm_uri]
|
92
|
+
else
|
93
|
+
# unencrypted file
|
94
|
+
end
|
95
|
+
|
96
|
+
begin
|
97
|
+
read_proc = DRM::HttpProcs.read_proc drm_uri
|
98
|
+
|
99
|
+
unwrapper = DRM::Unwrapper.new read_proc
|
100
|
+
metadata = unwrapper.metadata
|
101
|
+
rescue URI::InvalidURIError
|
102
|
+
metadata = nil
|
103
|
+
end
|
104
|
+
|
105
|
+
if metadata.nil?
|
106
|
+
send_headers(socket, 404)
|
107
|
+
return
|
108
|
+
end
|
109
|
+
|
110
|
+
pp metadata
|
111
|
+
|
112
|
+
key_fname = @key_prefix + File.basename(real_uri) + '.drm-tem'
|
113
|
+
proc = Tem::SecPack.new_from_yaml_str(File.open(key_fname, 'rb') { |f| f.read })
|
114
|
+
subkey_proc = DRM::Tem.subkey_proc_for_tem_proc(proc)
|
115
|
+
|
116
|
+
data_queue = Queue.new
|
117
|
+
write_proc = Proc.new do |data|
|
118
|
+
data_queue.enq data
|
119
|
+
end
|
120
|
+
|
121
|
+
length = metadata.length
|
122
|
+
if headers['Range']
|
123
|
+
# parse content range, send in relevant stuff
|
124
|
+
units, ranges_str = *headers['Range'].split('=', 2)
|
125
|
+
if units != 'bytes'
|
126
|
+
send_headers(socket, 416, 0, 'text/html', 'Content-Range' => "0-#{length - 1}/#{length}")
|
127
|
+
return
|
128
|
+
end
|
129
|
+
|
130
|
+
ranges = ranges_str.split(',').map do |srange|
|
131
|
+
# break up and deal with negative values
|
132
|
+
srange_ends = srange.split('-',2).map! { |s| (s.nil? or s.empty?) ? -1 : s.to_i }
|
133
|
+
if srange_ends[0] < 0
|
134
|
+
srange_ends = [length - srange_ends[1], length - 1]
|
135
|
+
elsif srange_ends[1] < 0
|
136
|
+
srange_ends[1] = length - 1
|
137
|
+
end
|
138
|
+
# clamp
|
139
|
+
srange_ends.map! { |i| (i < length) ? i : length - 1 }
|
140
|
+
end
|
141
|
+
|
142
|
+
# cheat and reply w/ first range 'cause multi-part responses are hard
|
143
|
+
send_headers(socket, 206, ranges[0][1] + 1 - ranges[0][0], metadata.mime_type, 'Accept-Ranges' => 'bytes', 'ETag' => metadata.etag, 'Content-Range' => "bytes #{ranges[0][0]}-#{ranges[0][1]}/#{length}")
|
144
|
+
Thread.new do
|
145
|
+
|
146
|
+
unwrapper.unwrap ranges[0][0], ranges[0][1], read_proc, write_proc, subkey_proc
|
147
|
+
data_queue.enq nil
|
148
|
+
end
|
149
|
+
else
|
150
|
+
send_headers(socket, 200, length, metadata.mime_type, 'Accept-Ranges' => 'bytes', 'ETag' => metadata.etag)
|
151
|
+
Thread.new do
|
152
|
+
unwrapper.unwrap 0, length, read_proc, write_proc, subkey_proc
|
153
|
+
data_queue.enq nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
loop do
|
158
|
+
data = data_queue.deq
|
159
|
+
break if data.nil?
|
160
|
+
|
161
|
+
sent_data = 0
|
162
|
+
sent_data += socket.send(data[sent_data..-1], 0) while sent_data < data.length
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def send_headers(socket, status, length = 0, mime_type = 'text/html', headers = {})
|
167
|
+
reason = headers[:reason] || {200 => 'OK', 404 => 'File not found'}[status]
|
168
|
+
|
169
|
+
response_line = "HTTP/1.1 #{status} #{reason}\r\n"
|
170
|
+
|
171
|
+
response_headers = { 'Connection' => 'close', 'Server' => 'Rubylicious/1.0', 'Date' => Time.now.rfc2822,
|
172
|
+
'Content-Type' => mime_type, 'Content-Length' => length }.merge headers
|
173
|
+
|
174
|
+
pp response_headers
|
175
|
+
socket.send response_line, 0
|
176
|
+
socket.send response_headers.to_a.map { |k,v| k.to_s + ': ' + v.to_s }.join("\r\n") + "\r\n\r\n", 0
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
STDOUT.sync = true
|
181
|
+
if File.exists? 'drm_proxy.yml'
|
182
|
+
conf = File.open('drm_proxy.yml', 'r') { |f| YAML::load f }
|
183
|
+
else
|
184
|
+
conf = { :port => 8080, :key_prefix => './files/' }
|
185
|
+
File.open('drm_proxy.yml', 'w') { |f| YAML::dump conf, f }
|
186
|
+
end
|
187
|
+
|
188
|
+
def connect_tem
|
189
|
+
creation_proc = Kernel.proc { $terminal = Tem::SCard::JCOPRemoteTerminal.new }
|
190
|
+
connect_proc = Kernel.proc { creation_proc.call(); $terminal.connect }
|
191
|
+
|
192
|
+
unless connect_proc.call()
|
193
|
+
$terminal.disconnect
|
194
|
+
creation_proc = Kernel.proc { $terminal = Tem::SCard::PCSCTerminal.new }
|
195
|
+
connect_proc.call()
|
196
|
+
end
|
197
|
+
$javacard = Tem::SCard::JavaCard.new($terminal)
|
198
|
+
$tem = Tem::Session.new($javacard)
|
199
|
+
class <<$tem
|
200
|
+
def issue_apdu(*args)
|
201
|
+
loop do
|
202
|
+
begin
|
203
|
+
return super(*args)
|
204
|
+
rescue
|
205
|
+
print $! + "\n"
|
206
|
+
print $!.backtrace.join("\n") + "\n"
|
207
|
+
connect_proc.call()
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
connect_tem
|
215
|
+
|
216
|
+
server = DRM::Proxy.new conf
|
217
|
+
|
218
|
+
print "Listening on port #{conf[:port]}\n"
|
219
|
+
sleep 1 while true
|
data/bin/drm_unwrap.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'drm'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
gem 'tem_ruby'
|
6
|
+
require 'tem_ruby'
|
7
|
+
|
8
|
+
unless ARGV.length == 3
|
9
|
+
print "Usage: #{$0} encrypted_file key_file decrypted_file\n"
|
10
|
+
exit
|
11
|
+
end
|
12
|
+
|
13
|
+
encrypted_fname = ARGV[0]
|
14
|
+
key_fname = ARGV[1]
|
15
|
+
decrypted_fname = ARGV[2]
|
16
|
+
begin
|
17
|
+
read_proc = DRM::HttpProcs.read_proc encrypted_fname
|
18
|
+
pp 'http ok'
|
19
|
+
rescue URI::InvalidURIError
|
20
|
+
read_proc = DRM::IoProcs.read_proc File.open(encrypted_fname, 'rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
if key_fname =~ /\.drm\-tem$/
|
24
|
+
proc = Tem::SecPack.new_from_yaml_str(File.open(key_fname, 'rb') { |f| f.read })
|
25
|
+
subkey_proc = DRM::Tem.subkey_proc_for_tem_proc(proc)
|
26
|
+
|
27
|
+
$terminal = Tem::SCard::JCOPRemoteTerminal.new
|
28
|
+
unless $terminal.connect
|
29
|
+
$terminal.disconnect
|
30
|
+
$terminal = Tem::SCard::PCSCTerminal.new
|
31
|
+
$terminal.connect
|
32
|
+
end
|
33
|
+
$javacard = Tem::SCard::JavaCard.new($terminal)
|
34
|
+
$tem = Tem::Session.new($javacard)
|
35
|
+
elsif key_fname =~ /\.drm\-key$/
|
36
|
+
key = File.open(key_fname, 'rb') { |f| f.read }
|
37
|
+
subkey_proc = Proc.new do |metadata, key_index|
|
38
|
+
metadata.subkey_from_filekey key_index, key
|
39
|
+
end
|
40
|
+
else
|
41
|
+
print "Unknown key file format in #{key_fname} (need .drm-tem and .drm-key)\n"
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
File.open(decrypted_fname, 'wb') do |outfile|
|
46
|
+
unwrapper = DRM::Unwrapper.new read_proc
|
47
|
+
unwrapper.unwrap 0, unwrapper.metadata.length - 1, read_proc, DRM::IoProcs.write_proc(outfile), subkey_proc
|
48
|
+
end
|
data/bin/drm_wrap.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'drm'
|
3
|
+
|
4
|
+
unless ARGV.length == 1
|
5
|
+
print "Usage: #{$0} file\n"
|
6
|
+
exit
|
7
|
+
end
|
8
|
+
|
9
|
+
original_fname = ARGV[0]
|
10
|
+
encrypted_fname = ARGV[0] + '.drm-enc'
|
11
|
+
key_fname = ARGV[0] + '.drm-key'
|
12
|
+
|
13
|
+
in_length = File.stat(original_fname).size
|
14
|
+
wrapper = DRM::Wrapper.new in_length, 'audio/mp3'
|
15
|
+
|
16
|
+
File.open(key_fname, 'wb') { |f| f.write wrapper.filekey }
|
17
|
+
|
18
|
+
File.open(original_fname, 'rb') do |infile|
|
19
|
+
File.open(encrypted_fname, 'wb') do |outfile|
|
20
|
+
wrapper.wrap DRM::IoProcs.read_proc(infile), DRM::IoProcs.write_proc(outfile)
|
21
|
+
end
|
22
|
+
end
|
data/lib/drm/drm.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module DRM::HttpProcs
|
5
|
+
# produces a read_proc(length, offset) for the given HTTP +url+
|
6
|
+
def self.read_proc(url)
|
7
|
+
uri = URI.parse url
|
8
|
+
Proc.new do |length, offset|
|
9
|
+
request = Net::HTTP::Get.new uri.path
|
10
|
+
request.set_range(offset, length)
|
11
|
+
response = Net::HTTP.start(uri.host, uri.port) { |http| http.request request }
|
12
|
+
# content_range_str = nil
|
13
|
+
# response.each_header { |key, value| content_range_str = value and break if key.downcase == 'content-range' }
|
14
|
+
response.body
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/drm/io_procs.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module DRM::IoProcs
|
2
|
+
# produces a read_proc(length, offset) for the given +io+ instance of IO
|
3
|
+
def self.read_proc(io)
|
4
|
+
Proc.new do |length, offset|
|
5
|
+
io.seek offset
|
6
|
+
io.read length
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# produces a write_proc(data) for the given +io+ instance of IO
|
11
|
+
def self.write_proc(io)
|
12
|
+
Proc.new do |data|
|
13
|
+
io.write data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/drm/metadata.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'openssl'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
class DRM::Metadata
|
6
|
+
# internal constructor; access through new_for_file and new_from_hash
|
7
|
+
def initialize(data_hash)
|
8
|
+
@data = data_hash
|
9
|
+
end
|
10
|
+
|
11
|
+
# metadata object from a hash obtained using to_hash
|
12
|
+
def self.new_from_hash(data_hash)
|
13
|
+
# TODO: some validation to ensure the hash contains the metadata fields
|
14
|
+
self.new data_hash
|
15
|
+
end
|
16
|
+
|
17
|
+
# metadata object from a string obtained using to_yaml_str
|
18
|
+
def self.new_from_yaml_str(yaml_str)
|
19
|
+
data_hash = YAML.load yaml_str
|
20
|
+
# TODO: verify we're actually dealing with a hash
|
21
|
+
self.new_from_hash data_hash
|
22
|
+
end
|
23
|
+
|
24
|
+
# new metadata object for a content file
|
25
|
+
def self.new_for_file(length, mime_type, options = {})
|
26
|
+
# storage strategy
|
27
|
+
data_hash = { :length => length, :mime_type => mime_type }
|
28
|
+
blocksize = options[:block_size]
|
29
|
+
if blocksize.nil?
|
30
|
+
blocksize = 1 << 16
|
31
|
+
# TODO: adaptive block size
|
32
|
+
end
|
33
|
+
data_hash[:block_size] = blocksize
|
34
|
+
|
35
|
+
# encryption strategy
|
36
|
+
keys = options[:keys] || 8
|
37
|
+
# TODO: adaptive number of keys
|
38
|
+
data_hash[:keys] = keys
|
39
|
+
data_hash[:blocks] = (length + blocksize - 1) / blocksize
|
40
|
+
data_hash[:blocks_per_key] = (data_hash[:blocks] + keys - 1) / keys
|
41
|
+
|
42
|
+
# serial number
|
43
|
+
data_hash[:etag] = Digest::SHA1.hexdigest((0...32).map { |i| rand(256) }.pack('C*'))
|
44
|
+
|
45
|
+
self.new data_hash
|
46
|
+
end
|
47
|
+
|
48
|
+
# creates a new file key
|
49
|
+
def self.new_filekey
|
50
|
+
(0...32).map { |i| rand * 256 }.pack('C*')
|
51
|
+
end
|
52
|
+
|
53
|
+
# converts the metadata to an easy-to-serialize hash
|
54
|
+
def to_hash
|
55
|
+
@data
|
56
|
+
end
|
57
|
+
|
58
|
+
# converts the metadata to an easy-to-serialize to_yaml_str
|
59
|
+
def to_yaml_str
|
60
|
+
self.to_hash.to_yaml.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
# computes the index of the subkey to be used for decoding a block
|
64
|
+
def subkey_index(block_index)
|
65
|
+
block_index / @data[:blocks_per_key]
|
66
|
+
end
|
67
|
+
|
68
|
+
# computers a subkey from a file key (this will usually happen on a TEM)
|
69
|
+
def subkey_from_filekey(subkey_index, filekey)
|
70
|
+
Digest::SHA1.digest(filekey + [subkey_index].pack('N'))
|
71
|
+
end
|
72
|
+
|
73
|
+
# the number of blocks in the content file
|
74
|
+
def blocks
|
75
|
+
@data[:blocks]
|
76
|
+
end
|
77
|
+
|
78
|
+
# the size of a content block
|
79
|
+
def block_size
|
80
|
+
@data[:block_size]
|
81
|
+
end
|
82
|
+
|
83
|
+
# the length of the content
|
84
|
+
def length
|
85
|
+
@data[:length]
|
86
|
+
end
|
87
|
+
|
88
|
+
# the MIME type of the content
|
89
|
+
def mime_type
|
90
|
+
@data[:mime_type]
|
91
|
+
end
|
92
|
+
|
93
|
+
# the ETag of the document
|
94
|
+
def etag
|
95
|
+
@data[:etag]
|
96
|
+
end
|
97
|
+
|
98
|
+
# encrypts a content block
|
99
|
+
def encrypt_block(block_data, block_index, subkey)
|
100
|
+
cipher = OpenSSL::Cipher::Cipher.new 'aes-128-ecb'
|
101
|
+
cipher.encrypt
|
102
|
+
cipher.key = subkey
|
103
|
+
cipher.iv = [block_index].pack('N') * 4
|
104
|
+
eblock = cipher.update(block_data)
|
105
|
+
return eblock
|
106
|
+
end
|
107
|
+
|
108
|
+
# decrypts a content block
|
109
|
+
def decrypt_block(crypted_data, block_index, subkey)
|
110
|
+
cipher = OpenSSL::Cipher::Cipher.new 'aes-128-ecb'
|
111
|
+
cipher.decrypt
|
112
|
+
cipher.key = subkey
|
113
|
+
cipher.iv = [block_index].pack('N') * 4
|
114
|
+
dblock = cipher.update(crypted_data)
|
115
|
+
# compensate for openssl being retarded
|
116
|
+
dblock += cipher.update([0].pack('C') * 16)
|
117
|
+
return dblock
|
118
|
+
end
|
119
|
+
end
|
data/lib/drm/tem.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'tem_ruby'
|
3
|
+
require 'tem_ruby'
|
4
|
+
|
5
|
+
module DRM::Tem
|
6
|
+
# produces a TEM procedure which yields the subkeys for +filekey+
|
7
|
+
# the procedure is sealed with the TEM public key +pubek+
|
8
|
+
def self.tem_proc_for_filekey(filekey, pubek)
|
9
|
+
key_proc = Tem::Session.assemble do |p|
|
10
|
+
p.ldbc filekey.length + 4
|
11
|
+
p.outnew
|
12
|
+
p.mdfxb :from => :filekey, :size => filekey.length + 4, :to => (1 << 16) - 1
|
13
|
+
p.halt
|
14
|
+
p.label :filekey
|
15
|
+
p.immed :ubyte, filekey.unpack('C*')
|
16
|
+
p.label :key_index
|
17
|
+
p.filler :ubyte, 2
|
18
|
+
p.filler :ushort, 2
|
19
|
+
p.label :end_of_hash
|
20
|
+
p.stack
|
21
|
+
p.extra 8
|
22
|
+
end
|
23
|
+
key_proc.seal pubek, :filekey, :key_index
|
24
|
+
return key_proc
|
25
|
+
end
|
26
|
+
|
27
|
+
# produces a subkey_proc(metadata, index) that works by calling a TEM with +tem_proc+
|
28
|
+
# +tem_proc+ must be obtained by calling tem_proc_for_filekey
|
29
|
+
def self.subkey_proc_for_tem_proc(tem_proc)
|
30
|
+
Proc.new do |metadata, index|
|
31
|
+
index_str = [0, 0] + Tem::Session.to_tem_ushort(index)
|
32
|
+
tem_proc.body[tem_proc.label_address(:key_index), index_str.length] = index_str
|
33
|
+
# TODO: more elegant solution than $tem
|
34
|
+
$tem.execute(tem_proc).pack('C*')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class DRM::Unwrapper
|
2
|
+
# new unwrapper for a DRM-wrapped file
|
3
|
+
# read_proc(length, offset) reads length bytes at offset from the DRM-wrapped file
|
4
|
+
def initialize(read_proc)
|
5
|
+
@data_offset = [0].pack('N').length
|
6
|
+
metadata_lenstr = read_proc.call @data_offset, 0
|
7
|
+
metadata_str = read_proc.call metadata_lenstr.unpack('N').first, @data_offset
|
8
|
+
@data_offset += metadata_str.length
|
9
|
+
@metadata = DRM::Metadata.new_from_yaml_str metadata_str
|
10
|
+
end
|
11
|
+
|
12
|
+
# the DRM metadata for the wrapped file
|
13
|
+
attr_reader :metadata
|
14
|
+
|
15
|
+
# unwraps bytes +start_byte+..+end_byte+ from the content of a DRM-wrapped file
|
16
|
+
# read_proc(length, offset) reads length bytes at offset from the file to be wrapped
|
17
|
+
# write_proc(data) appends the given data to the file containing the wrapped content
|
18
|
+
# subkey_proc(metadata, index) produces the +index+th DRM subkey for the contents identified by +metadata+
|
19
|
+
def unwrap(start_byte, end_byte, read_proc, write_proc, subkey_proc)
|
20
|
+
block_size = @metadata.block_size
|
21
|
+
start_block = start_byte / block_size
|
22
|
+
end_block = end_byte / block_size
|
23
|
+
|
24
|
+
in_offset = @data_offset + start_block * block_size
|
25
|
+
old_ski = nil
|
26
|
+
subkey = nil
|
27
|
+
start_block.upto(end_block) do |block_index|
|
28
|
+
# read
|
29
|
+
crypted_data = read_proc.call block_size, in_offset
|
30
|
+
in_offset += crypted_data.length
|
31
|
+
# decrypt
|
32
|
+
ski = @metadata.subkey_index(block_index)
|
33
|
+
subkey = subkey_proc.call @metadata, ski unless ski == old_ski
|
34
|
+
old_ski = ski
|
35
|
+
block_data = @metadata.decrypt_block crypted_data, block_index, subkey
|
36
|
+
# write
|
37
|
+
write_proc.call block_data[((block_index == start_block) ? start_byte % block_size : 0)..((block_index == end_block) ? end_byte % block_size : block_size - 1)]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/drm/wrapper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class DRM::Wrapper
|
2
|
+
# new wrapper for a contents file
|
3
|
+
# the file has +length+ bytes and its MIME type is +mime_type+
|
4
|
+
def initialize(length, mime_type)
|
5
|
+
@filekey = DRM::Metadata.new_filekey
|
6
|
+
@metadata = DRM::Metadata.new_for_file length, mime_type
|
7
|
+
end
|
8
|
+
|
9
|
+
# the key used to wrap the file
|
10
|
+
attr_reader :filekey
|
11
|
+
# the DRM metadata for the wrapped file
|
12
|
+
attr_reader :metadata
|
13
|
+
|
14
|
+
# wraps a content file with DRM protection
|
15
|
+
# read_proc(length, offset) reads length bytes at offset from the file to be wrapped
|
16
|
+
# write_proc(data) appends the given data to the file containing the wrapped content
|
17
|
+
def wrap(read_proc, write_proc)
|
18
|
+
# push the metadata
|
19
|
+
metadata_str = @metadata.to_yaml_str
|
20
|
+
metadata_lenstr = [metadata_str.length].pack('N')
|
21
|
+
write_proc.call metadata_lenstr
|
22
|
+
write_proc.call metadata_str
|
23
|
+
|
24
|
+
# push the data
|
25
|
+
block_size = @metadata.block_size
|
26
|
+
length = @metadata.length
|
27
|
+
in_offset = 0
|
28
|
+
0.upto(@metadata.blocks - 1) do |block_index|
|
29
|
+
# read
|
30
|
+
read_size = (block_size <= length - in_offset) ? block_size : length - in_offset
|
31
|
+
block_data = read_proc.call read_size, in_offset
|
32
|
+
in_offset += read_size
|
33
|
+
# encrypt
|
34
|
+
subkey = @metadata.subkey_from_filekey(metadata.subkey_index(block_index), @filekey)
|
35
|
+
crypted_data = @metadata.encrypt_block block_data, block_index, subkey
|
36
|
+
# write
|
37
|
+
write_proc.call crypted_data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/tem_drm.gemspec
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
# Gem::Specification for Tem_drm-0.0.1
|
3
|
+
# Originally generated by Echoe
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = %q{tem_drm}
|
7
|
+
s.version = "0.0.1"
|
8
|
+
|
9
|
+
s.specification_version = 2 if s.respond_to? :specification_version=
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.authors = ["Victor Costan"]
|
13
|
+
s.date = %q{2008-06-11}
|
14
|
+
s.description = %q{Personal DRM (Digital Rights Management) relying on the TEM.}
|
15
|
+
s.email = %q{victor@costan.us}
|
16
|
+
s.executables = ["drm_wrap.rb", "drm_key2proc.rb", "drm_unwrap.rb", "drm_proxy.rb"]
|
17
|
+
s.extra_rdoc_files = ["bin/drm_wrap.rb", "bin/drm_key2proc.rb", "bin/drm_unwrap.rb", "bin/drm_proxy.rb", "LICENSE", "lib/drm/unwrapper.rb", "lib/drm/wrapper.rb", "lib/drm/tem.rb", "lib/drm/io_procs.rb", "lib/drm/metadata.rb", "lib/drm/http_procs.rb", "lib/drm/drm.rb", "README", "CHANGELOG"]
|
18
|
+
s.files = ["bin/drm_wrap.rb", "bin/drm_key2proc.rb", "bin/drm_unwrap.rb", "bin/drm_proxy.rb", "LICENSE", "test/kick_vlc.xspf", "lib/drm/unwrapper.rb", "lib/drm/wrapper.rb", "lib/drm/tem.rb", "lib/drm/io_procs.rb", "lib/drm/metadata.rb", "lib/drm/http_procs.rb", "lib/drm/drm.rb", "README", "CHANGELOG", "Manifest", "tem_drm.gemspec"]
|
19
|
+
s.has_rdoc = true
|
20
|
+
s.homepage = %q{http://tem.rubyforge.org}
|
21
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Tem_drm", "--main", "README"]
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
s.rubyforge_project = %q{tem}
|
24
|
+
s.rubygems_version = %q{1.1.1}
|
25
|
+
s.summary = %q{Personal DRM (Digital Rights Management) relying on the TEM.}
|
26
|
+
|
27
|
+
s.add_dependency(%q<tem_ruby>, [">= 0.9.0"])
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
# # Original Rakefile source (requires the Echoe gem):
|
32
|
+
#
|
33
|
+
# require 'rubygems'
|
34
|
+
# gem 'echoe'
|
35
|
+
# require 'echoe'
|
36
|
+
#
|
37
|
+
# Echoe.new('tem_drm') do |p|
|
38
|
+
# p.project = 'tem' # rubyforge project
|
39
|
+
#
|
40
|
+
# p.author = 'Victor Costan'
|
41
|
+
# p.email = 'victor@costan.us'
|
42
|
+
# p.summary = 'Personal DRM (Digital Rights Management) relying on the TEM.'
|
43
|
+
# p.url = 'http://tem.rubyforge.org'
|
44
|
+
# p.dependencies = ['tem_ruby >=0.9.0']
|
45
|
+
#
|
46
|
+
# p.need_tar_gz = false
|
47
|
+
# p.rdoc_pattern = /^(lib|bin|tasks|ext)|^BUILD|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# if $0 == __FILE__
|
51
|
+
# Rake.application = Rake::Application.new
|
52
|
+
# Rake.application.run
|
53
|
+
# end
|
data/test/kick_vlc.xspf
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<playlist version="0" xmlns="http://xspf.org/ns/0/">
|
3
|
+
<title>kick_vlc.xspf</title>
|
4
|
+
<location>file:///Users/victor/Documents/workspace/tem_drm/kick_vlc.xspf</location>
|
5
|
+
<trackList>
|
6
|
+
<track>
|
7
|
+
<location>http://localhost:8080/?url=http:%252F%252Fweb.mit.edu%252Fcostan%252Fwww%252Fbooks%252FTocarte.Toa.mp3</location>
|
8
|
+
<title>Tocarte Toa</title>
|
9
|
+
</track>
|
10
|
+
</trackList>
|
11
|
+
</playlist>
|
metadata
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tem_drm
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Victor Costan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-06-11 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: tem_ruby
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.9.0
|
23
|
+
version:
|
24
|
+
description: Personal DRM (Digital Rights Management) relying on the TEM.
|
25
|
+
email: victor@costan.us
|
26
|
+
executables:
|
27
|
+
- drm_wrap.rb
|
28
|
+
- drm_key2proc.rb
|
29
|
+
- drm_unwrap.rb
|
30
|
+
- drm_proxy.rb
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files:
|
34
|
+
- bin/drm_wrap.rb
|
35
|
+
- bin/drm_key2proc.rb
|
36
|
+
- bin/drm_unwrap.rb
|
37
|
+
- bin/drm_proxy.rb
|
38
|
+
- LICENSE
|
39
|
+
- lib/drm/unwrapper.rb
|
40
|
+
- lib/drm/wrapper.rb
|
41
|
+
- lib/drm/tem.rb
|
42
|
+
- lib/drm/io_procs.rb
|
43
|
+
- lib/drm/metadata.rb
|
44
|
+
- lib/drm/http_procs.rb
|
45
|
+
- lib/drm/drm.rb
|
46
|
+
- README
|
47
|
+
- CHANGELOG
|
48
|
+
files:
|
49
|
+
- bin/drm_wrap.rb
|
50
|
+
- bin/drm_key2proc.rb
|
51
|
+
- bin/drm_unwrap.rb
|
52
|
+
- bin/drm_proxy.rb
|
53
|
+
- LICENSE
|
54
|
+
- test/kick_vlc.xspf
|
55
|
+
- lib/drm/unwrapper.rb
|
56
|
+
- lib/drm/wrapper.rb
|
57
|
+
- lib/drm/tem.rb
|
58
|
+
- lib/drm/io_procs.rb
|
59
|
+
- lib/drm/metadata.rb
|
60
|
+
- lib/drm/http_procs.rb
|
61
|
+
- lib/drm/drm.rb
|
62
|
+
- README
|
63
|
+
- CHANGELOG
|
64
|
+
- Manifest
|
65
|
+
- tem_drm.gemspec
|
66
|
+
has_rdoc: true
|
67
|
+
homepage: http://tem.rubyforge.org
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options:
|
70
|
+
- --line-numbers
|
71
|
+
- --inline-source
|
72
|
+
- --title
|
73
|
+
- Tem_drm
|
74
|
+
- --main
|
75
|
+
- README
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: "0"
|
83
|
+
version:
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: "0"
|
89
|
+
version:
|
90
|
+
requirements: []
|
91
|
+
|
92
|
+
rubyforge_project: tem
|
93
|
+
rubygems_version: 1.1.1
|
94
|
+
signing_key:
|
95
|
+
specification_version: 2
|
96
|
+
summary: Personal DRM (Digital Rights Management) relying on the TEM.
|
97
|
+
test_files: []
|
98
|
+
|