origami 1.0.4 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +40 -49
- data/bin/gui/about.rb +1 -1
- data/bin/pdfencrypt +10 -4
- data/origami/actions.rb +0 -1
- data/origami/docmdp.rb +96 -0
- data/origami/encryption.rb +103 -14
- data/origami/graphics.rb +1 -0
- data/origami/graphics/colors.rb +23 -23
- data/origami/graphics/instruction.rb +7 -29
- data/origami/graphics/path.rb +53 -19
- data/origami/graphics/render.rb +69 -0
- data/origami/graphics/state.rb +9 -9
- data/origami/graphics/text.rb +66 -48
- data/origami/graphics/xobject.rb +65 -50
- data/origami/javascript.rb +135 -0
- data/origami/metadata.rb +24 -0
- data/origami/object.rb +1 -1
- data/origami/page.rb +12 -0
- data/origami/parsers/pdf/linear.rb +0 -0
- data/origami/pdf.rb +2 -2
- metadata +77 -75
- data/VERSION +0 -1
data/README
CHANGED
@@ -1,73 +1,64 @@
|
|
1
|
-
|
2
|
-
# #
|
3
|
-
# Origami - Ruby PDF manipulation framework #
|
4
|
-
# #
|
5
|
-
################################################################################
|
1
|
+
NAME
|
6
2
|
|
7
|
-
|
8
|
-
==============
|
3
|
+
origami
|
9
4
|
|
10
|
-
|
11
|
-
documents.
|
12
|
-
This is *NOT* a PDF rendering library, it aims at providing a scripting tool for
|
13
|
-
generating and analyzing malicious PDF files. As well, it can be used to create
|
14
|
-
on-the-fly customized PDFs, or to inject evil code into already existing
|
15
|
-
documents.
|
5
|
+
DESCRIPTION
|
16
6
|
|
7
|
+
Origami is a framework written in Ruby designed to parse, analyze, and forge
|
8
|
+
PDF documents. This is not a PDF rendering library, it aims at providing a
|
9
|
+
scripting tool for generating and analyzing malicious PDF files. As well, it
|
10
|
+
can be used to create on-the-fly customized PDFs, or to inject evil code into
|
11
|
+
already existing documents.
|
17
12
|
|
18
|
-
|
19
|
-
==========
|
13
|
+
VERSION
|
20
14
|
|
21
|
-
|
22
|
-
See the COPYING.LESSER file for more details.
|
15
|
+
1.1
|
23
16
|
|
17
|
+
DEPENDENCIES
|
24
18
|
|
25
|
-
|
26
|
-
==========
|
19
|
+
- Ruby-GTK2 (only for GUI), http://ruby-gnome2.sourceforge.jp/
|
27
20
|
|
28
|
-
|
21
|
+
INSTALL
|
29
22
|
|
23
|
+
Stable: gem install origami
|
24
|
+
Devel: hg clone https://origami-pdf.googlecode.com/hg/ origami
|
30
25
|
|
31
|
-
|
32
|
-
===============
|
26
|
+
DIRECTORIES
|
33
27
|
|
34
|
-
|
35
|
-
|
28
|
+
``origami/``
|
29
|
+
Core scripts used to parse a PDF file. All objects and features are
|
30
|
+
provided here.
|
36
31
|
|
32
|
+
``samples/``
|
33
|
+
Many samples, mostly sorted to generate specially crafted PDFs.
|
37
34
|
|
38
|
-
|
39
|
-
|
35
|
+
``bin/``
|
36
|
+
Useful tools based on Origami.
|
40
37
|
|
41
|
-
``
|
42
|
-
|
43
|
-
provided here.
|
38
|
+
``tests/``
|
39
|
+
Test case units.
|
44
40
|
|
45
|
-
``
|
46
|
-
|
41
|
+
``doc/``
|
42
|
+
Automated RubyDoc HTML documentation.
|
47
43
|
|
48
|
-
|
49
|
-
* Useful tools based on Origami.
|
44
|
+
HOMEPAGE
|
50
45
|
|
51
|
-
|
52
|
-
* Test case units.
|
46
|
+
http://aslr.fr/pages/Origami
|
53
47
|
|
54
|
-
|
55
|
-
* Automated RubyDoc HTML documentation.
|
48
|
+
CONTRIBUTORS
|
56
49
|
|
50
|
+
Guillaume Delugré <guillaume (at) security-labs.org> - Author
|
51
|
+
Frédéric Raynal <fred (at) security-labs.org> - Contributor
|
57
52
|
|
58
|
-
|
59
|
-
===============
|
53
|
+
LICENSE
|
60
54
|
|
61
|
-
|
62
|
-
|
55
|
+
This software is distributed under the LGPL license.
|
56
|
+
See the COPYING.LESSER file for more details.
|
63
57
|
|
58
|
+
NOTES
|
64
59
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
<guillaume [at] security-labs [dot] org>, with a short explanation of
|
70
|
-
what you did and any necessary PDF documents.
|
71
|
-
|
72
|
-
Thanks.
|
60
|
+
It contains many bugs and many incomplete features. If you encounter a
|
61
|
+
problem, feel free to report it by mail at <guillaume [at] security-labs
|
62
|
+
[dot] org>, with a short explanation of what you did and any necessary PDF
|
63
|
+
documents. Thanks.
|
73
64
|
|
data/bin/gui/about.rb
CHANGED
@@ -36,7 +36,7 @@ module PDFWalker
|
|
36
36
|
:version => Origami::VERSION,
|
37
37
|
:copyright => "Copyright (C) 2010\nGuillaume Delugre, Sogeti-ESEC R&D <guillaume@security-labs.org>\nAll right reserved.",
|
38
38
|
:comments => "A graphical PDF parser front-end",
|
39
|
-
:license => File.read("#{File.dirname(
|
39
|
+
:license => File.read("#{File.dirname(__FILE__)}/../../COPYING.LESSER")
|
40
40
|
})
|
41
41
|
|
42
42
|
end
|
data/bin/pdfencrypt
CHANGED
@@ -37,7 +37,7 @@ require 'optparse'
|
|
37
37
|
|
38
38
|
class OptParser
|
39
39
|
BANNER = <<USAGE
|
40
|
-
Usage: #{$0} [<PDF-file>] [-p <password>] [-c <cipher>] [-s <key-size>] [-o <output-file>]
|
40
|
+
Usage: #{$0} [<PDF-file>] [-p <password>] [-c <cipher>] [-s <key-size>] [--hardened] [-o <output-file>]
|
41
41
|
Encrypts a PDF document. Supports RC4 40 to 128 bits, AES128, AES256.
|
42
42
|
Bug reports or feature requests at: http://origami-pdf.googlecode.com/
|
43
43
|
|
@@ -64,6 +64,10 @@ USAGE
|
|
64
64
|
options[:key_size] = s.to_i
|
65
65
|
end
|
66
66
|
|
67
|
+
opts.on("--hardened", "Use stronger key validation scheme (only AES-256)") do
|
68
|
+
options[:hardened] = true
|
69
|
+
end
|
70
|
+
|
67
71
|
opts.on_tail("-h", "--help", "Show this message") do
|
68
72
|
puts opts
|
69
73
|
exit
|
@@ -77,7 +81,8 @@ USAGE
|
|
77
81
|
:output => STDOUT,
|
78
82
|
:password => '',
|
79
83
|
:cipher => 'aes',
|
80
|
-
:key_size => 128
|
84
|
+
:key_size => 128,
|
85
|
+
:hardened => false
|
81
86
|
}
|
82
87
|
|
83
88
|
self.parser(options).parse!(args)
|
@@ -100,13 +105,14 @@ begin
|
|
100
105
|
:user_password => @options[:password],
|
101
106
|
:owner_password => @options[:password],
|
102
107
|
:cipher => @options[:cipher],
|
103
|
-
:key_size => @options[:key_size]
|
108
|
+
:key_size => @options[:key_size],
|
109
|
+
:hardened => @options[:hardened]
|
104
110
|
)
|
105
111
|
pdf.save(@options[:output], :noindent => true)
|
106
112
|
|
107
113
|
rescue SystemExit
|
108
114
|
rescue Exception => e
|
109
|
-
STDERR.puts "#{e.class}: #{e.message}"
|
115
|
+
STDERR.puts "#{e.class}: #{e.message} #{e.backtrace}"
|
110
116
|
exit 1
|
111
117
|
end
|
112
118
|
|
data/origami/actions.rb
CHANGED
data/origami/docmdp.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
=begin
|
2
|
+
|
3
|
+
= File
|
4
|
+
docmdp.rb
|
5
|
+
|
6
|
+
= Info
|
7
|
+
This file is part of Origami, PDF manipulation framework for Ruby
|
8
|
+
Copyright (C) 2010 Guillaume Delugr� <guillaume@security-labs.org>
|
9
|
+
All right reserved.
|
10
|
+
|
11
|
+
Origami is free software: you can redistribute it and/or modify
|
12
|
+
it under the terms of the GNU Lesser General Public License as published by
|
13
|
+
the Free Software Foundation, either version 3 of the License, or
|
14
|
+
(at your option) any later version.
|
15
|
+
|
16
|
+
Origami is distributed in the hope that it will be useful,
|
17
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
18
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
19
|
+
GNU Lesser General Public License for more details.
|
20
|
+
|
21
|
+
You should have received a copy of the GNU Lesser General Public License
|
22
|
+
along with Origami. If not, see <http://www.gnu.org/licenses/>.
|
23
|
+
|
24
|
+
=end
|
25
|
+
|
26
|
+
module Origami
|
27
|
+
|
28
|
+
class Null
|
29
|
+
def to_docmdp_str
|
30
|
+
"\000"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Integer
|
35
|
+
def to_docmdp_str
|
36
|
+
[ 1, self.value & 0xFFFFFFFF ].pack("CN")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Real
|
41
|
+
def to_docmdp_str
|
42
|
+
[ 2, self.value.round & 0xFFFFFFFF ].pack("CN")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Boolean
|
47
|
+
def to_docmdp_str
|
48
|
+
[ 3, (self.false?) ? 0 : 1 ].pack("CN")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Name
|
53
|
+
def to_docmdp_str
|
54
|
+
[ 4, self.to_s.length, self.to_s ].pack("CNA*")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class String
|
59
|
+
def to_docmdp_str
|
60
|
+
[ 5, self.to_s.length, self.to_s ].pack("CNA*")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class Dictionary
|
65
|
+
def to_docmdp_str(*fields)
|
66
|
+
if fields.empty?
|
67
|
+
self.each_pair { |key, value|
|
68
|
+
|
69
|
+
}
|
70
|
+
else
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Array
|
77
|
+
def to_docmdp_str
|
78
|
+
str = [ 7, self.length ].pack("CN")
|
79
|
+
|
80
|
+
self.each do |obj|
|
81
|
+
str << obj.to_docmdp_str
|
82
|
+
end
|
83
|
+
|
84
|
+
str
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Stream
|
89
|
+
def to_docmdp_str
|
90
|
+
[ 8, self.dictionary.size ].pack("CN") +
|
91
|
+
self.dictionary.to_docmdp_str(:DecodeParms, :F, :FDecodeParms, :FFilter, :Filter, :Length) +
|
92
|
+
[ self.rawdata.length, self.rawdata ].pack("NA*")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
data/origami/encryption.rb
CHANGED
@@ -49,7 +49,6 @@ module Origami
|
|
49
49
|
|
50
50
|
#
|
51
51
|
# Decrypts the current document (only RC4 40..128 bits).
|
52
|
-
# TODO: AESv3
|
53
52
|
# _passwd_:: The password to decrypt the document.
|
54
53
|
#
|
55
54
|
def decrypt(passwd = "")
|
@@ -209,6 +208,7 @@ module Origami
|
|
209
208
|
:owner_passwd => '',
|
210
209
|
:cipher => 'rc4', # :RC4 or :AES
|
211
210
|
:key_size => 128, # Key size in bits
|
211
|
+
:hardened => false, # Use newer password validation (since Reader X)
|
212
212
|
:encrypt_metadata => true, # Metadata shall be encrypted?
|
213
213
|
:permissions => Encryption::Standard::Permissions::ALL # Document permissions
|
214
214
|
}.update(options)
|
@@ -234,7 +234,12 @@ module Origami
|
|
234
234
|
if params[:key_size] == 128
|
235
235
|
version = revision = 4
|
236
236
|
elsif params[:key_size] == 256
|
237
|
-
version =
|
237
|
+
version = 5
|
238
|
+
if params[:hardened]
|
239
|
+
revision = 6
|
240
|
+
else
|
241
|
+
revision = 5
|
242
|
+
end
|
238
243
|
else
|
239
244
|
raise EncryptionError, "Invalid AES key length (Only 128 and 256 bits keys are supported)"
|
240
245
|
end
|
@@ -1069,6 +1074,14 @@ module Origami
|
|
1069
1074
|
field :Perms, :Type => String, :Version => '1.7', :ExtensionLevel => 3
|
1070
1075
|
field :P, :Type => Integer, :Default => 0, :Required => true
|
1071
1076
|
field :EncryptMetadata, :Type => Boolean, :Default => true, :Version => "1.5"
|
1077
|
+
|
1078
|
+
def pdf_version_required #:nodoc:
|
1079
|
+
if self.R > 5
|
1080
|
+
[ 1.7, 8 ]
|
1081
|
+
else
|
1082
|
+
super
|
1083
|
+
end
|
1084
|
+
end
|
1072
1085
|
|
1073
1086
|
#
|
1074
1087
|
# Computes the key that will be used to encrypt/decrypt the document contents with user password.
|
@@ -1099,7 +1112,12 @@ module Origami
|
|
1099
1112
|
passwd = password_to_utf8(userpassword)
|
1100
1113
|
|
1101
1114
|
uks = self.U[40, 8]
|
1102
|
-
|
1115
|
+
|
1116
|
+
if self.R == 5
|
1117
|
+
ukey = Digest::SHA256.digest(passwd + uks)
|
1118
|
+
else
|
1119
|
+
ukey = compute_hardened_hash(passwd, uks)
|
1120
|
+
end
|
1103
1121
|
|
1104
1122
|
iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")
|
1105
1123
|
AES.new(ukey, nil, false).decrypt(iv + self.UE.value)
|
@@ -1108,15 +1126,20 @@ module Origami
|
|
1108
1126
|
|
1109
1127
|
#
|
1110
1128
|
# Computes the key that will be used to encrypt/decrypt the document contents with owner password.
|
1111
|
-
# Revision 5
|
1129
|
+
# Revision 5 and above.
|
1112
1130
|
#
|
1113
1131
|
def compute_owner_encryption_key(ownerpassword)
|
1114
|
-
if self.R
|
1132
|
+
if self.R >= 5
|
1115
1133
|
passwd = password_to_utf8(ownerpassword)
|
1116
1134
|
|
1117
1135
|
oks = self.O[40, 8]
|
1118
|
-
|
1119
|
-
|
1136
|
+
|
1137
|
+
if self.R == 5
|
1138
|
+
okey = Digest::SHA256.digest(passwd + oks)
|
1139
|
+
else
|
1140
|
+
okey = compute_hardened_hash(passwd, oks)
|
1141
|
+
end
|
1142
|
+
|
1120
1143
|
iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")
|
1121
1144
|
AES.new(okey, nil, false).decrypt(iv + self.OE.value)
|
1122
1145
|
end
|
@@ -1136,21 +1159,32 @@ module Origami
|
|
1136
1159
|
self.O = owner_key
|
1137
1160
|
self.U = compute_user_password(userpassword, salt)
|
1138
1161
|
|
1139
|
-
|
1162
|
+
else
|
1140
1163
|
upass = password_to_utf8(userpassword)
|
1141
1164
|
opass = password_to_utf8(ownerpassword)
|
1142
1165
|
|
1143
1166
|
uvs, uks, ovs, oks = ::Array.new(4) { ::Array.new(8) { rand(255) }.pack("C*") }
|
1144
1167
|
file_key = ::Array.new(32) { rand(256) }.pack("C*")
|
1145
1168
|
iv = ::Array.new(AES::BLOCKSIZE, 0).pack("C*")
|
1146
|
-
|
1147
|
-
ukey = Digest::SHA256.digest(upass + uks)
|
1148
|
-
okey = Digest::SHA256.digest(opass + oks)
|
1149
1169
|
|
1170
|
+
if self.R == 5
|
1171
|
+
ukey = Digest::SHA256.digest(upass + uks)
|
1172
|
+
okey = Digest::SHA256.digest(opass + oks)
|
1173
|
+
else
|
1174
|
+
ukey = compute_hardened_hash(upass, uks)
|
1175
|
+
okey = compute_hardened_hash(upass, oks)
|
1176
|
+
end
|
1177
|
+
|
1150
1178
|
self.UE = AES.new(ukey, iv, false).encrypt(file_key)[iv.size, 32]
|
1151
1179
|
self.OE = AES.new(okey, iv, false).encrypt(file_key)[iv.size, 32]
|
1152
|
-
|
1153
|
-
|
1180
|
+
|
1181
|
+
if self.R == 5
|
1182
|
+
self.U = Digest::SHA256.digest(upass + uvs) + uvs + uks
|
1183
|
+
self.O = Digest::SHA256.digest(opass + ovs + self.U) + ovs + oks
|
1184
|
+
else
|
1185
|
+
self.U = compute_hardened_hash(upass, uvs) + uvs + uks
|
1186
|
+
self.O = compute_hardened_hash(opass, ovs, self.U) + ovs + oks
|
1187
|
+
end
|
1154
1188
|
|
1155
1189
|
perms =
|
1156
1190
|
[ self.P ].pack("V") + # 0-3
|
@@ -1168,7 +1202,7 @@ module Origami
|
|
1168
1202
|
#
|
1169
1203
|
# Checks user password.
|
1170
1204
|
# For version 2,3 and 4, _salt_ is the document ID.
|
1171
|
-
# For version 5, _salt_ is the User Key Salt.
|
1205
|
+
# For version 5 and 6, _salt_ is the User Key Salt.
|
1172
1206
|
#
|
1173
1207
|
def is_user_password?(pass, salt)
|
1174
1208
|
|
@@ -1179,6 +1213,9 @@ module Origami
|
|
1179
1213
|
elsif self.R == 5
|
1180
1214
|
uvs = self.U[32, 8]
|
1181
1215
|
Digest::SHA256.digest(pass + uvs) == self.U[0, 32]
|
1216
|
+
elsif self.R == 6
|
1217
|
+
uvs = self.U[32, 8]
|
1218
|
+
compute_hardened_hash(pass, uvs) == self.U[0, 32]
|
1182
1219
|
end
|
1183
1220
|
end
|
1184
1221
|
|
@@ -1195,6 +1232,9 @@ module Origami
|
|
1195
1232
|
elsif self.R == 5
|
1196
1233
|
ovs = self.O[32, 8]
|
1197
1234
|
Digest::SHA256.digest(pass + ovs + self.U) == self.O[0, 32]
|
1235
|
+
elsif self.R == 6
|
1236
|
+
ovs = self.O[32, 8]
|
1237
|
+
compute_hardened_hash(pass, ovs, self.U[0,48]) == self.O[0, 32]
|
1198
1238
|
end
|
1199
1239
|
end
|
1200
1240
|
|
@@ -1258,6 +1298,55 @@ module Origami
|
|
1258
1298
|
user_key.ljust(32, "\xFF")
|
1259
1299
|
end
|
1260
1300
|
end
|
1301
|
+
|
1302
|
+
#
|
1303
|
+
# Computes hardened hash used in revision 6 (extension level 8).
|
1304
|
+
#
|
1305
|
+
def compute_hardened_hash(password, salt, vector = '')
|
1306
|
+
block_size = 32
|
1307
|
+
input = Digest::SHA256.digest(password + salt + vector) + "\x00" * 32
|
1308
|
+
key = input[0, 16]
|
1309
|
+
iv = input[16, 16]
|
1310
|
+
digest, aes, h, x = nil, nil, nil, nil
|
1311
|
+
|
1312
|
+
i = 0
|
1313
|
+
while i < 64 or i < x[-1].ord + 32
|
1314
|
+
j = 0
|
1315
|
+
block = input[0, block_size]
|
1316
|
+
|
1317
|
+
if Origami::OPTIONS[:use_openssl]
|
1318
|
+
aes = OpenSSL::Cipher::Cipher.new("aes-128-cbc").encrypt
|
1319
|
+
aes.iv = iv
|
1320
|
+
aes.key = key
|
1321
|
+
aes.padding = 0
|
1322
|
+
else
|
1323
|
+
fail "You need OpenSSL support to encrypt/decrypt documents with this method"
|
1324
|
+
end
|
1325
|
+
|
1326
|
+
64.times do |j|
|
1327
|
+
x = aes.update(block)
|
1328
|
+
unless vector.empty?
|
1329
|
+
x += aes.update(vector)
|
1330
|
+
end
|
1331
|
+
|
1332
|
+
if j == 0
|
1333
|
+
block_size = 32 + (x.unpack("C16").inject(0) {|a,b| a+b} % 3) * 16
|
1334
|
+
digest = Digest::SHA2.new(block_size << 3)
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
digest.update(x)
|
1338
|
+
end
|
1339
|
+
|
1340
|
+
h = digest.digest
|
1341
|
+
key = h[0, 16]
|
1342
|
+
input[0, block_size] = h[0, block_size]
|
1343
|
+
iv = h[16, 16]
|
1344
|
+
|
1345
|
+
i = i + 1
|
1346
|
+
end
|
1347
|
+
|
1348
|
+
h[0, 32]
|
1349
|
+
end
|
1261
1350
|
|
1262
1351
|
def xor(str, byte) #:nodoc:
|
1263
1352
|
str.split(//).map!{|c| (c[0].ord ^ byte).chr }.join
|