xref: /trunk/test/odf-validate/decrypt.py (revision 0e16a31d78a652c56c5c0fc79c752f48b9048df9)
1#**************************************************************
2#
3#  Licensed to the Apache Software Foundation (ASF) under one
4#  or more contributor license agreements.  See the NOTICE file
5#  distributed with this work for additional information
6#  regarding copyright ownership.  The ASF licenses this file
7#  to you under the Apache License, Version 2.0 (the
8#  "License"); you may not use this file except in compliance
9#  with the License.  You may obtain a copy of the License at
10#
11#    http://www.apache.org/licenses/LICENSE-2.0
12#
13#  Unless required by applicable law or agreed to in writing,
14#  software distributed under the License is distributed on an
15#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16#  KIND, either express or implied.  See the License for the
17#  specific language governing permissions and limitations
18#  under the License.
19#
20#**************************************************************
21
22# usage: decrypt.py <in-dir> <out-dir> <password>      (decrypt.py --selftest runs the cipher known-answer tests)
23# Writes a plain copy of every package in <in-dir> into <out-dir> for the schema checkers: encrypted
24# streams are decrypted and inflated, the manifest loses its encryption-data elements and the manifest:size
25# of those entries. Do not run pkgcheck.py on the copies. A checksum mismatch (wrong password) or any other
26# failure writes nothing for that package and makes the exit status 1.
27# Implements what main/package writes and reads: PBKDF2-HMAC-SHA1 over the start key (SHA1 or SHA256 of the
28# UTF-8 password), Blowfish with 64 bit feedback (rtl_cipher_CFB over 8 byte blocks) or AES-CBC with W3C padding.
29import sys, os, io, re, zlib, zipfile, hashlib, base64, struct
30from xml.etree import ElementTree as ET
31
32M = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
33Q = "{%s}" % M
34ET.register_namespace("manifest", M)
35MASK = 0xFFFFFFFF
36
37
38class DecryptError(Exception):
39    pass
40
41
42def pi_hex_words(n):
43    bits = 32 * n + 64
44    one = 1 << (bits + 16)
45
46    def arctan_inv(x):
47        x2, term, total, k = x * x, one // x, 0, 0
48        while term:
49            total += term // (2 * k + 1) if k % 2 == 0 else -(term // (2 * k + 1))
50            term //= x2
51            k += 1
52        return total
53    pi = 16 * arctan_inv(5) - 4 * arctan_inv(239)
54    frac = (pi - (3 * one)) >> 16
55    frac >>= 64
56    return [(frac >> (32 * (n - 1 - i))) & MASK for i in range(n)]
57
58
59_BF_INIT = []
60
61
62class Blowfish:
63    def __init__(self, key):
64        if not _BF_INIT:
65            _BF_INIT.extend(pi_hex_words(18 + 1024))
66            assert _BF_INIT[0] == 0x243F6A88 and _BF_INIT[18] == 0xD1310BA6 and _BF_INIT[-1] == 0x3AC372E6
67        self.P = _BF_INIT[:18]
68        self.S = [_BF_INIT[18 + 256 * i:18 + 256 * (i + 1)] for i in range(4)]
69        k = 0
70        for i in range(18):
71            d = 0
72            for _ in range(4):
73                d = (d << 8) | key[k]
74                k = (k + 1) % len(key)
75            self.P[i] ^= d
76        l = r = 0
77        for i in range(0, 18, 2):
78            l, r = self.encrypt(l, r)
79            self.P[i], self.P[i + 1] = l, r
80        for i in range(4):
81            for j in range(0, 256, 2):
82                l, r = self.encrypt(l, r)
83                self.S[i][j], self.S[i][j + 1] = l, r
84
85    def encrypt(self, l, r):
86        P, S0, S1, S2, S3 = self.P, *self.S
87        for i in range(16):
88            l ^= P[i]
89            r ^= ((((S0[l >> 24] + S1[(l >> 16) & 255]) & MASK) ^ S2[(l >> 8) & 255]) + S3[l & 255]) & MASK
90            l, r = r, l
91        l, r = r, l
92        return l ^ P[17], r ^ P[16]
93
94    def encrypt_block(self, b):
95        return struct.pack(">II", *self.encrypt(*struct.unpack(">II", b)))
96
97
98def blowfish_cfb64_decrypt(key, iv, data):
99    bf = Blowfish(key)
100    reg = (iv + bytes(8))[:8]
101    out = bytearray()
102    for i in range(0, len(data), 8):
103        ct = data[i:i + 8]
104        ks = bf.encrypt_block(reg)
105        out += bytes(a ^ b for a, b in zip(ct, ks))
106        reg = ct
107    return bytes(out)
108
109
110def _aes_tables():
111    def xt(a):
112        a <<= 1
113        return (a ^ 0x11B) & 255 if a & 256 else a
114
115    def mul(a, b):
116        r = 0
117        while b:
118            if b & 1:
119                r ^= a
120            a, b = xt(a), b >> 1
121        return r
122    inv = [0] * 256
123    for a in range(1, 256):
124        for b in range(1, 256):
125            if mul(a, b) == 1:
126                inv[a] = b
127                break
128    sbox = []
129    for a in range(256):
130        x = inv[a]
131        s = x ^ (x << 1 | x >> 7) & 255 ^ (x << 2 | x >> 6) & 255 ^ (x << 3 | x >> 5) & 255 ^ (x << 4 | x >> 4) & 255 ^ 0x63
132        sbox.append(s & 255)
133    isbox = [0] * 256
134    for i, s in enumerate(sbox):
135        isbox[s] = i
136    return sbox, isbox, {n: [mul(a, n) for a in range(256)] for n in (2, 9, 11, 13, 14)}
137
138
139_AES = []
140
141
142def aes_schedule(key):
143    if not _AES:
144        _AES.extend(_aes_tables())
145    sbox, mt = _AES[0], _AES[2]
146    if len(key) not in (16, 24, 32):
147        raise DecryptError("AES key size %d" % len(key))
148    nk = len(key) // 4
149    nr = nk + 6
150    w = [list(key[4 * i:4 * i + 4]) for i in range(nk)]
151    rcon = 1
152    for i in range(nk, 4 * (nr + 1)):
153        t = list(w[i - 1])
154        if i % nk == 0:
155            t = [sbox[b] for b in t[1:] + t[:1]]
156            t[0] ^= rcon
157            rcon = mt[2][rcon]
158        elif nk > 6 and i % nk == 4:
159            t = [sbox[b] for b in t]
160        w.append([a ^ b for a, b in zip(w[i - nk], t)])
161    return [sum(w[4 * r:4 * r + 4], []) for r in range(nr + 1)]
162
163
164def aes_decrypt_block(rk, blk):
165    isbox, mt = _AES[1], _AES[2]
166    m9, m11, m13, m14 = mt[9], mt[11], mt[13], mt[14]
167    nr = len(rk) - 1
168    s = [a ^ b for a, b in zip(blk, rk[nr])]
169    for r in range(nr - 1, -1, -1):
170        s = [s[0], s[13], s[10], s[7], s[4], s[1], s[14], s[11], s[8], s[5], s[2], s[15], s[12], s[9], s[6], s[3]]
171        s = [isbox[b] ^ k for b, k in zip(s, rk[r])]
172        if r:
173            t = []
174            for c in range(0, 16, 4):
175                a0, a1, a2, a3 = s[c:c + 4]
176                t += [m14[a0] ^ m11[a1] ^ m13[a2] ^ m9[a3], m9[a0] ^ m14[a1] ^ m11[a2] ^ m13[a3],
177                      m13[a0] ^ m9[a1] ^ m14[a2] ^ m11[a3], m11[a0] ^ m13[a1] ^ m9[a2] ^ m14[a3]]
178            s = t
179    return bytes(s)
180
181
182def aes_cbc_decrypt(key, iv, data):
183    rk = aes_schedule(key)
184    if len(data) % 16 or not data:
185        raise DecryptError("AES data is not a multiple of 16 bytes")
186    out, prev = bytearray(), iv
187    for off in range(0, len(data), 16):
188        blk = data[off:off + 16]
189        out += bytes(a ^ b for a, b in zip(aes_decrypt_block(rk, blk), prev))
190        prev = blk
191    n = out[-1]
192    if not 1 <= n <= 16:
193        raise DecryptError("bad AES padding byte %d" % n)
194    return bytes(out[:-n])
195
196
197def sha1_rtl(data):
198    """SHA-1 as main/sal rtl_digest_SHA1 computes it: input lengths of 52..55 mod 64 get one extra padding block."""
199    h = [0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]
200    r = len(data) % 64
201    m = data + b"\x80" + bytes((55 - r) % 64) + (bytes(64) if 52 <= r <= 55 else b"") + struct.pack(">Q", len(data) * 8)
202
203    def rol(x, n):
204        return ((x << n) | (x >> (32 - n))) & MASK
205    for o in range(0, len(m), 64):
206        w = list(struct.unpack(">16I", m[o:o + 64]))
207        for i in range(16, 80):
208            w.append(rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1))
209        a, b, c, d, e = h
210        for i in range(80):
211            if i < 20:
212                f, k = (b & c) | (~b & d), 0x5A827999
213            elif i < 40:
214                f, k = b ^ c ^ d, 0x6ED9EBA1
215            elif i < 60:
216                f, k = (b & c) | (b & d) | (c & d), 0x8F1BBCDC
217            else:
218                f, k = b ^ c ^ d, 0xCA62C1D6
219            a, b, c, d, e = (rol(a, 5) + f + e + k + w[i]) & MASK, a, rol(b, 30), c, d
220        h = [(x + y) & MASK for x, y in zip(h, (a, b, c, d, e))]
221    return struct.pack(">5I", *h)
222
223
224def b64(v, what):
225    try:
226        return base64.b64decode(v or "", validate=True)
227    except Exception:
228        raise DecryptError("%s is not base64: %r" % (what, v))
229
230
231def entry_params(fe):
232    ed = fe.find(Q + "encryption-data")
233    alg, kd, sk = ed.find(Q + "algorithm"), ed.find(Q + "key-derivation"), ed.find(Q + "start-key-generation")
234    if alg is None or kd is None:
235        raise DecryptError("encryption-data lacks algorithm or key-derivation")
236    p = {"checksum": b64(ed.get(Q + "checksum"), "checksum") if ed.get(Q + "checksum") else None}
237    ctype = ed.get(Q + "checksum-type")
238    p["check"] = {"SHA1/1K": "sha1", M + "#sha1-1k": "sha1", M + "#sha256-1k": "sha256"}.get(ctype)
239    if p["check"] is None:
240        raise DecryptError("checksum-type %r" % ctype)
241    name = alg.get(Q + "algorithm-name")
242    keylen = {"http://www.w3.org/2001/04/xmlenc#aes128-cbc": 16, "http://www.w3.org/2001/04/xmlenc#aes192-cbc": 24,
243              "http://www.w3.org/2001/04/xmlenc#aes256-cbc": 32}.get(name)
244    if name in ("Blowfish CFB", M + "#blowfish"):
245        p["cipher"], keylen = "blowfish", 16
246    elif keylen:
247        p["cipher"] = "aes"
248    else:
249        raise DecryptError("algorithm-name %r" % name)
250    p["iv"] = b64(alg.get(Q + "initialisation-vector"), "initialisation-vector")
251    if kd.get(Q + "key-derivation-name") not in ("PBKDF2", M + "#pbkdf2"):
252        raise DecryptError("key-derivation-name %r" % kd.get(Q + "key-derivation-name"))
253    p["salt"] = b64(kd.get(Q + "salt"), "salt")
254    p["iter"] = int(kd.get(Q + "iteration-count"))
255    p["keylen"] = int(kd.get(Q + "key-size") or keylen)
256    sname = sk.get(Q + "start-key-generation-name") if sk is not None else "SHA1"
257    p["start"] = {"SHA1": "sha1", "http://www.w3.org/2000/09/xmldsig#sha1": "sha1",
258                  "http://www.w3.org/2000/09/xmldsig#sha256": "sha256"}.get(sname)
259    if p["start"] is None:
260        raise DecryptError("start-key-generation-name %r" % sname)
261    return p
262
263
264def raw_entry(path, zi):
265    with open(path, "rb") as f:
266        f.seek(zi.header_offset)
267        h = f.read(30)
268        if h[:4] != b"PK\x03\x04":
269            raise DecryptError("no local header for %s" % zi.filename)
270        n, e = struct.unpack("<HH", h[26:30])
271        f.seek(zi.header_offset + 30 + n + e)
272        return f.read(zi.compress_size)
273
274
275def decrypt_entry(raw, p, password, name, notes):
276    start = hashlib.new(p["start"], password.encode("utf-8")).digest()
277    key = hashlib.pbkdf2_hmac("sha1", start, p["salt"], p["iter"], p["keylen"])
278    try:
279        plain = blowfish_cfb64_decrypt(key, p["iv"], raw) if p["cipher"] == "blowfish" else aes_cbc_decrypt(key, p["iv"], raw)
280    except DecryptError as e:
281        raise DecryptError("%s: %s (wrong password?)" % (name, e))
282    if p["checksum"] is not None and hashlib.new(p["check"], plain[:1024]).digest() != p["checksum"]:
283        if p["check"] == "sha1" and sha1_rtl(plain[:1024]) == p["checksum"]:
284            notes.append("%s: checksum is the non-standard SHA-1 of main/sal (hashed length %d, mod 64 = %d)"
285                         % (name, len(plain[:1024]), len(plain[:1024]) % 64))
286        else:
287            raise DecryptError("%s: checksum mismatch (wrong password?)" % name)
288    try:
289        d = zlib.decompressobj(-15)
290        out = d.decompress(plain) + d.flush()
291    except zlib.error as e:
292        raise DecryptError("%s: decrypted data does not inflate (%s)" % (name, e))
293    if not d.eof:
294        raise DecryptError("%s: deflate stream is truncated" % name)
295    return out
296
297
298def decrypt_package(src, dst, password, notes):
299    """Returns the number of encrypted entries decrypted; 0 means the package was copied unchanged."""
300    z = zipfile.ZipFile(src)
301    mani = ET.fromstring(z.read("META-INF/manifest.xml"))
302    enc = {}
303    for fe in mani.findall(Q + "file-entry"):
304        if fe.find(Q + "encryption-data") is not None:
305            enc[fe.get(Q + "full-path")] = fe
306    data = {}
307    for zi in z.infolist():
308        n = zi.filename
309        if n in enc:
310            if zi.compress_type != zipfile.ZIP_STORED:
311                raise DecryptError("%s: encrypted entry is not STORED" % n)
312            plain = decrypt_entry(raw_entry(src, zi), entry_params(enc[n]), password, n, notes)
313            size = enc[n].get(Q + "size")
314            if size is not None and int(size) != len(plain):
315                raise DecryptError("%s: manifest:size %s, decrypted %d bytes" % (n, size, len(plain)))
316            data[n] = plain
317        else:
318            data[n] = z.read(n)
319    for fe in enc.values():
320        fe.remove(fe.find(Q + "encryption-data"))
321        fe.attrib.pop(Q + "size", None)
322        fe.text = None
323    missing = [p for p in enc if p not in data]
324    if missing:
325        raise DecryptError("manifest lists encrypted entries not in the zip: %s" % missing)
326    if enc:
327        data["META-INF/manifest.xml"] = ET.tostring(mani, encoding="UTF-8", xml_declaration=True)
328    tmp = dst + ".tmp"
329    with zipfile.ZipFile(tmp, "w") as o:
330        names = [zi.filename for zi in z.infolist()]
331        for n in sorted(names, key=lambda n: n != "mimetype"):
332            zi = zipfile.ZipInfo(n, date_time=z.getinfo(n).date_time)
333            zi.compress_type = zipfile.ZIP_STORED if n == "mimetype" or n.endswith("/") else zipfile.ZIP_DEFLATED
334            zi.external_attr = z.getinfo(n).external_attr
335            o.writestr(zi, data[n])
336    os.replace(tmp, dst)
337    return len(enc)
338
339
340def selftest():
341    bad = 0
342
343    def check(name, got, want):
344        nonlocal bad
345        print(("ok   " if got == want else "FAIL ") + name)
346        bad += got != want
347    for k, pt, ct in (("0000000000000000", "0000000000000000", "4ef997456198dd78"),
348                      ("ffffffffffffffff", "ffffffffffffffff", "51866fd5b85ecb8a"),
349                      ("3000000000000000", "1000000000000001", "7d856f9a613063f2")):
350        check("blowfish key " + k, Blowfish(bytes.fromhex(k)).encrypt_block(bytes.fromhex(pt)).hex(), ct)
351    pt = "00112233445566778899aabbccddeeff"
352    for k, ct in (("000102030405060708090a0b0c0d0e0f", "69c4e0d86a7b0430d8cdb78070b4c55a"),
353                  ("000102030405060708090a0b0c0d0e0f1011121314151617", "dda97ca4864cdfe06eaf70a0ec0d7191"),
354                  ("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "8ea2b7ca516745bfeafc49904b496089")):
355        check("aes-%d block" % (len(k) * 2), aes_decrypt_block(aes_schedule(bytes.fromhex(k)), bytes.fromhex(ct)).hex(), pt)
356    msgs = [(bytes(range(256)) * 2)[:n] for n in range(0, 300) if not 52 <= n % 64 <= 55]
357    check("sha1_rtl equals SHA-1 outside 52..55 mod 64", all(sha1_rtl(m) == hashlib.sha1(m).digest() for m in msgs), True)
358    check("sha1_rtl differs from SHA-1 at 52..55 mod 64",
359          all(sha1_rtl(bytes(n)) != hashlib.sha1(bytes(n)).digest() for n in (52, 53, 54, 55, 116, 886)), True)
360    check("pbkdf2-hmac-sha1 RFC 6070", hashlib.pbkdf2_hmac("sha1", b"password", b"salt", 2, 20).hex(),
361          "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957")
362    return bad == 0
363
364
365def main(argv):
366    if len(argv) == 2 and argv[1] == "--selftest":
367        return 0 if selftest() else 1
368    if len(argv) != 4:
369        print("usage: decrypt.py <in-dir> <out-dir> <password>", file=sys.stderr)
370        return 2
371    src, dst, password = argv[1:]
372    os.makedirs(dst, exist_ok=True)
373    done = copied = 0
374    failed, quirks = [], 0
375    for name in sorted(os.listdir(src)):
376        path = os.path.join(src, name)
377        if name.startswith(".") or not os.path.isfile(path) or not zipfile.is_zipfile(path):
378            print("skipped (not a zip package): %s" % name)
379            continue
380        notes = []
381        try:
382            n = decrypt_package(path, os.path.join(dst, name), password, notes)
383        except (DecryptError, zipfile.BadZipFile, KeyError, ET.ParseError, ValueError) as e:
384            failed.append(name)
385            print("FAILED %s: %s" % (name, e), file=sys.stderr)
386            continue
387        for note in notes:
388            quirks += 1
389            print("note %s: %s" % (name, note))
390        if n:
391            done += 1
392            print("decrypted %s (%d encrypted entries)" % (name, n))
393        else:
394            copied += 1
395            print("copied %s (no encrypted entries)" % name)
396    print("%d decrypted, %d copied unchanged, %d failed, %d non-standard SHA-1 checksums" % (done, copied, len(failed), quirks))
397    return 1 if failed else 0
398
399
400if __name__ == "__main__":
401    sys.exit(main(sys.argv))
402