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"""Password-protected documents. The office saves documents of each application with a password, under each combination of the 21UseSHA1InODF12 and UseBlowfishInODF12 settings, in ODF 1.2 strict and extended mode. The ported decryptor (aootest.checkers.decrypt, 22which reimplements PBKDF2, Blowfish and AES independently of AOO) must then read them: the manifest names the algorithms the setting 23asks for, the right password decrypts every entry and passes the checksum, a wrong one is rejected, and the decrypted content equals 24the same document saved without a password, apart from the cached value of date and time fields.""" 25import zipfile 26 27import pytest 28 29from aootest import stability 30from aootest.checkers import decrypt 31 32pytestmark = pytest.mark.slow 33 34PASSWORD = "aootest-Password" 35MODES = {"strict": 4, "ext": 3} 36# variant -> (UseSHA1InODF12, UseBlowfishInODF12) 37VARIANTS = {"default": (True, True), "aes": (False, False), "sha1aes": (True, False), "sha256bf": (False, True)} 38FULL = [("odt", False), ("ods", False), ("odp", False), ("odg", False), ("odt", True), ("ods", True), ("odp", True), ("odg", True)] 39BASIC = [("odt", False), ("odt", True)] 40CASES = [(m, v, k, o) for m in MODES for v in VARIANTS for k, o in (FULL if v in ("default", "aes") else BASIC)] 41IDS = ["%s-%s|%s%s" % (m, v, k, "-objects" if o else "") for m, v, k, o in CASES] 42 43 44@pytest.fixture(scope="module") 45def encrypted_runs(aoo_offices, tmp_path_factory): 46 """{(mode, variant, kind, objects): paths}, saving each document once, on first use.""" 47 work, done = tmp_path_factory.mktemp("encrypted"), {} 48 49 def get(mode, variant, kind, objects): 50 key = (mode, variant, kind, objects) 51 if key not in done: 52 sha1, blowfish = VARIANTS[variant] 53 session = aoo_offices.get(MODES[mode], {"UseSHA1InODF12": sha1, "UseBlowfishInODF12": blowfish}, "%s_%s" % (mode, variant)) 54 d = work / ("%s-%s" % (mode, variant)) / ("%s%s" % (kind, "-objects" if objects else "")) 55 d.mkdir(parents=True) 56 dst, twin = d / ("enc." + kind), d / ("plain." + kind) 57 session.call("make_encrypted", {"kind": kind, "dst": str(dst), "twin": str(twin), "password": PASSWORD, 58 "with_objects": objects}, timeout=300) 59 done[key] = {"dir": d, "enc": dst, "twin": twin} 60 return done[key] 61 return get 62 63 64def encrypted_entries(path): 65 """{entry name: manifest file-entry} of the encrypted entries of a package.""" 66 with zipfile.ZipFile(path) as z: 67 mani = decrypt.ET.fromstring(z.read("META-INF/manifest.xml")) 68 return {fe.get(decrypt.Q + "full-path"): fe for fe in mani.findall(decrypt.Q + "file-entry") 69 if fe.find(decrypt.Q + "encryption-data") is not None} 70 71 72@pytest.mark.parametrize("mode,variant,kind,objects", CASES, ids=IDS) 73class TestEncrypted: 74 def test_manifest_names_the_algorithms_of_the_setting(self, encrypted_runs, need_mode, mode, variant, kind, objects): 75 need_mode(MODES[mode]) 76 entries = encrypted_entries(encrypted_runs(mode, variant, kind, objects)["enc"]) 77 assert entries, "nothing in the package is encrypted" 78 assert "mimetype" not in entries and "META-INF/manifest.xml" not in entries 79 sha1, blowfish = VARIANTS[variant] 80 for name, fe in entries.items(): 81 p = decrypt.entry_params(fe) 82 assert (p["start"], p["cipher"]) == ("sha1" if sha1 else "sha256", "blowfish" if blowfish else "aes"), name 83 84 def test_right_password_decrypts(self, encrypted_runs, need_mode, mode, variant, kind, objects): 85 need_mode(MODES[mode]) 86 r = encrypted_runs(mode, variant, kind, objects) 87 n = decrypt.decrypt_package(str(r["enc"]), str(r["dir"] / "decrypted.zip"), PASSWORD, []) 88 assert n == len(encrypted_entries(r["enc"])) > 0 89 90 def test_wrong_password_is_rejected(self, encrypted_runs, need_mode, mode, variant, kind, objects): 91 need_mode(MODES[mode]) 92 r = encrypted_runs(mode, variant, kind, objects) 93 with pytest.raises(decrypt.DecryptError): 94 decrypt.decrypt_package(str(r["enc"]), str(r["dir"] / "wrong.zip"), "not-" + PASSWORD, []) 95 96 def test_decrypted_content_equals_the_unencrypted_twin(self, encrypted_runs, need_mode, mode, variant, kind, objects): 97 need_mode(MODES[mode]) 98 r = encrypted_runs(mode, variant, kind, objects) 99 decrypt.decrypt_package(str(r["enc"]), str(r["dir"] / "decrypted.zip"), PASSWORD, []) 100 with zipfile.ZipFile(r["dir"] / "decrypted.zip") as a, zipfile.ZipFile(r["twin"]) as b: 101 for part in ("content.xml", "styles.xml"): 102 assert stability.normalize(a.read(part).decode("utf-8")) == stability.normalize(b.read(part).decode("utf-8")), part 103