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