Skip to content

Commit 708f841

Browse files
authored
PKI Security Handler implementation (#69)
* Fixes two bugs from StdSecurity Handler 1. Crypto filters in the streams may not be properly handled when filters and params were not all CosArrays. 2. StdSecHandler failed to create the encryption key and iv files with lesser than the required number of bytes. Led to a few build failures as well. * PKI Security Handler for decrypting certificate based PDF files. * File may get locked in windows so forcing file closure. * Read the p12 file to a buffer. As long as the password is shredded it's not as severe a security flaw.
1 parent 123afb0 commit 708f841

File tree

8 files changed

+469
-186
lines changed

8 files changed

+469
-186
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "PDFIO"
22
uuid = "4d0d745f-9d9a-592e-8d18-1ad8a0f42b92"
33
authors = ["Sambit Kumar Dash<sambitdash@gmail.com> "]
4-
version = "0.1.6"
4+
version = "0.1.7"
55

66
[deps]
77
AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c"

docs/src/encrypt.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,29 @@ nor the file encryption key resident in memory. With the API
123123
being open source we do not expect to provide any protection against
124124
debugging.
125125

126+
### PKI Security Handler
127+
PKI security handler can be used to decrypt PDF documents if
128+
needed. The handler will be invoked when needed to decrypt data very
129+
similar to the standard security handler. The default method provides
130+
capabilities to decrypt the document using a PKCS#12 (.p12) file as a
131+
keystore. However, the default behavior can be easily overwritten by
132+
providing your own access function. The default code looks like below:
133+
134+
```
135+
function get_digital_id()
136+
p12file = ""
137+
while !isfile(p12file)
138+
p12file =
139+
prompt("Select the PCKS#12 (.p12) certificate for the recepient")
140+
end
141+
p12pass = getpass("Enter the password to open the PKCS#12 (.p12) file")
142+
return shred!(x->read_pkcs12(p12file, x), p12pass)
143+
end
144+
145+
doc = pdDocOpen("file.pdf", access=get_digital_id)
146+
```
147+
148+
The functionality has been developed with `OpenSSL`. OpenSSL `ENGINE`
149+
interface can be used to implement another version of `get_digital_id`
150+
method using a `PKCS#11` based interface to enable HSM or hardware
151+
tokens as certificate stores.

src/Cos.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@ include("CosObjStream.jl")
1515
include("CosDoc.jl")
1616
include("CosCrypt.jl")
1717
include("StdSecHandler.jl")
18-
18+
include("PKISecHandler.jl")
1919
end

src/CosCrypt.jl

Lines changed: 143 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,141 @@
1+
using Base: SecretBuffer, SecretBuffer!, shred!
2+
import ..Common: decrypt
3+
4+
function store_key(s::SecHandler, cfn::CosName,
5+
data::Tuple{UInt32, SecretBuffer})
6+
skey = SecretBuffer!(read(s.skey_path, 32))
7+
iv = SecretBuffer!(read(s.iv_path, 16))
8+
cctx = CipherContext("aes_256_cbc", skey, iv, true)
9+
shred!(skey); shred!(iv)
10+
11+
perm, key = data
12+
c = update!(cctx, key)
13+
append!(c, close(cctx))
14+
s.keys[cfn] = (perm, c)
15+
return data
16+
end
17+
18+
function get_key(s::SecHandler, cfn::CosName)
19+
permkey = get(s.keys, cfn, nothing)
20+
permkey === nothing && return nothing
21+
22+
skey = SecretBuffer!(read(s.skey_path, 32))
23+
iv = SecretBuffer!(read(s.iv_path, 16))
24+
cctx = CipherContext("aes_256_cbc", skey, iv, false)
25+
shred!(skey); shred!(iv)
26+
27+
perm, c = permkey
28+
b = update!(cctx, c)
29+
append!(b, close(cctx))
30+
return perm, SecretBuffer!(b)
31+
end
32+
33+
function get_cfm(h::SecHandler, cfn::CosName)
34+
cfn === cn"Identity" && return cn"None"
35+
cfm = get(get(h.cf, cfn), cn"CFM")
36+
cfm in [cn"None", cn"V2", cn"AESV2", cn"AESV3"] || error(E_INVALID_CRYPT)
37+
return cfm
38+
end
39+
140
struct CryptParams
241
num::Int
342
gen::Int
443
cfn::CosName
544
end
645

46+
CryptParams(h::SecHandler, oi::CosIndirectObject) =
47+
CryptParams(h, oi.num, oi.gen, oi.obj)
48+
49+
CryptParams(h::SecHandler, num::Int, gen::Int, o::CosObject) =
50+
h.r < 4 ? CryptParams(num, gen, cn"StdCF") : error(E_INVALID_OBJECT)
51+
52+
CryptParams(h::SecHandler, num::Int, gen::Int, o::CosString) =
53+
CryptParams(num, gen, h.strf)
54+
55+
CryptParams(h::SecHandler, num::Int, gen::Int, o::CosObjectStream) =
56+
CryptParams(h, num, gen, o.stm)
57+
58+
# For Crypt filter chain ensure the filters before the crypt filters are removed.
59+
# Crypt filter parameters are associated with the document and the CosStream as
60+
# such may not have access to such parameters. Hence, the crypt filter has to be
61+
# decrypted when the document information is available.
62+
function CryptParams(h::SecHandler, num::Int, gen::Int, o::CosStream)
63+
cfn = h.stmf
64+
filters = get(o, cn"FFilter")
65+
filters === CosNull && return CryptParams(num, gen, cfn)
66+
if cn"Crypt" === filters ||
67+
(filters isa CosArray && length(filters) > 0 && cn"Crypt" === filters[1])
68+
params = get(o, cn"FDecodeParms", CosDict())
69+
param = params isa CosDict ? params : params[1]
70+
cfn = get(param, cn"Name", cn"Identity")
71+
end
72+
return CryptParams(num, gen, cfn)
73+
end
74+
75+
function get_key(h::SecHandler, params::CryptParams)
76+
vpw = get_key(h, params.cfn)
77+
vpw === nothing || return vpw
78+
return get_key(h, params.cfn, h.access)
79+
end
80+
81+
function algo01(h::SecHandler, params::CryptParams,
82+
data::AbstractVector{UInt8}, isencrypt::Bool)
83+
num, gen, cfn = params.num, params.gen, params.cfn
84+
cfm = get_cfm(h, cfn)
85+
isRC4 = cfm === cn"V2"
86+
numarr = copy(reinterpret(UInt8, [num]))
87+
ENDIAN_BOM == 0x01020304 && reverse!(numarr)
88+
numarr = numarr[1:3]
89+
genarr = copy(reinterpret(UInt8, [gen]))
90+
ENDIAN_BOM == 0x01020304 && reverse!(genarr)
91+
genarr = genarr[1:2]
92+
perm, key = get_key(h, params)
93+
n = div(h.length, 8)
94+
md = shred!(key) do kc
95+
n != kc.size && error("Invalid encryption key length")
96+
seekend(kc); write(kc, numarr); write(kc, genarr)
97+
!isRC4 && write(kc, AES_SUFFIX)
98+
mdctx = DigestContext("md5")
99+
update!(mdctx, kc)
100+
return close(mdctx)
101+
end
102+
l = min(n+5, 16)
103+
key = SecretBuffer!(md[1:l])
104+
iv = SecretBuffer!(isRC4 ? UInt8[] :
105+
isencrypt ? crypto_random(16) : data[1:16])
106+
try
107+
cctx = CipherContext(isRC4 ? "rc4" : "aes_128_cbc", key, iv, isencrypt)
108+
d = (isRC4 || isencrypt) ? update!(cctx, data) :
109+
update!(cctx, (@view data[17:end]))
110+
append!(d, close(cctx))
111+
return d
112+
finally
113+
shred!(key); shred!(iv)
114+
end
115+
end
116+
117+
function algo01a(h::SecHandler, params::CryptParams,
118+
data::AbstractVector{UInt8}, isencrypt::Bool)
119+
perm, key = get_key(h, params)
120+
iv = SecretBuffer!(isencrypt ? crypto_random(16) : data[1:16])
121+
try
122+
cctx = CipherContext("aes_256_cbc", key, iv, isencrypt)
123+
d = isencrypt ? update!(cctx, data) : update!(cctx, (@view data[17:end]))
124+
append!(d, close(cctx))
125+
return d
126+
finally
127+
shred!(key); shred!(iv)
128+
end
129+
end
130+
131+
function crypt(h::SecHandler, params::CryptParams,
132+
data::Vector{UInt8}, isencrypt::Bool)
133+
cfm = get_cfm(h, params.cfn)
134+
cfm === cn"None" && return data
135+
cfm === cn"AESV3" && return algo01a(h, params, data, isencrypt)
136+
return algo01(h, params, data, isencrypt)
137+
end
138+
7139
decrypt!(::Union{Nothing, SecHandler}, obj) = obj
8140

9141
# Even when document has no security handler the object stream needs to be
@@ -50,29 +182,18 @@ function decrypt(h::SecHandler, params::CryptParams, o::CosStream)
50182
set!(o, cn"Length", CosInt(len))
51183

52184
filters = get(o, cn"FFilter")
53-
if filters !== CosNull
54-
if filters isa CosName
55-
if cn"Crypt" === filters
56-
set!(o, cn"FFilter", CosNull)
57-
set!(o, cn"FDecodeParms", CosNull)
58-
end
59-
else
60-
vf = get(filters)
61-
l = length(vf)
62-
if vf[1] === cn"Crypt"
63-
if l == 1
64-
set!(o, cn"FFilter", CosNull)
65-
set!(o, cn"FDecodeParms", CosNull)
66-
else
67-
filters = get(o, cn"FFilter")
68-
deleteat!(get(filters), 1)
69-
params = get(o, cn"FDecodeParms")
70-
params !== CosNull && deleteat!(get(params), 1)
71-
end
72-
end
73-
end
185+
if filters isa CosNullType ||
186+
(filters isa CosName && cn"Crypt" === filters) ||
187+
(filters isa CosArray &&
188+
(length(filters) == 0 ||
189+
(length(filters) == 1 && cn"Crypt" === filters[1])))
190+
set!(o, cn"FFilter", CosNull)
191+
set!(o, cn"FDecodeParms", CosNull)
192+
elseif filters isa CosArray && cn"Crypt" === filters[1]
193+
deleteat!(get(filters), 1)
194+
params = get(o, cn"FDecodeParms")
195+
params !== CosNull && deleteat!(get(params), 1)
74196
end
75-
76197
o.isInternal = false
77198
return o
78199
end

0 commit comments

Comments
 (0)