tem_drm 0.0.1
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.
- 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
|
+
|