origami 1.0.4 → 1.1.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/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
|