xref: /trunk/test/aoo-test/tests/odf/test_encrypted.py (revision 2cb6d6f119bbe2f78742f1c5c8538cb0d4e72ede)
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