#!/usr/bin/python2
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2015 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import parent
from testlib import *
from storagelib import *

class TestStorage(StorageCase):
    def testLuks(self):
        m = self.machine
        b = self.browser

        mount_point_secret = "/run/secret"

        self.login_and_go("/storage")

        # Add a disk and partition it
        m.add_disk("50M", serial="MYDISK")
        b.wait_in_text("#drives", "MYDISK")
        b.click('tr:contains("MYDISK")')
        b.wait_visible("#storage-detail")
        b.click('button:contains(Create partition table)')
        self.dialog({ "type": "gpt" })
        self.content_row_wait_in_col(1, 1, "Free Space")

        assert m.execute("grep -v ^# /etc/crypttab || true").strip() == ""

        # Format it with luks
        self.content_row_action(1, "Create Partition")
        self.dialog({ "size": 10,
                      "type": "luks+ext4",
                      "name": "ENCRYPTED",
                      "passphrase": "vainu-reku-toma-rolle-kaja",
                      "passphrase2": "vainu-reku-toma-rolle-kaja",
                      "store_passphrase": True,
                      "mounting": "custom",
                      "mount_point": mount_point_secret,
                      "crypto_extra_options": CheckBoxText("crypto,options") })
        self.content_row_wait_in_col(1, 1, "Encrypted data")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        if not self.storaged_is_old_udisks:
            self.wait_in_storaged_configuration(mount_point_secret)
            self.wait_in_storaged_configuration("crypto,options")
            # HACK: Put /etc/crypttab in the journal, in order to debug updating issues
            assert m.execute("cat /etc/crypttab | logger -s 2>&1 | grep 'UUID='") != ""
            assert m.execute("grep %s /etc/fstab" % mount_point_secret) != ""
            assert m.execute("cat /etc/luks-keys/*") == "vainu-reku-toma-rolle-kaja"

        # Lock it
        self.content_head_action(1, "Lock")
        b.wait_not_in_text("#detail-content", "ext4 File System")

        if not self.storaged_is_old_udisks:

            # Unlock, this uses the stored passphrase
            self.content_head_action(1, "Unlock")
            self.content_row_wait_in_col(2, 1, "ext4 File System")

            # Change options.  We keep trying until the stack has synched
            # up with crypttab and we see the old options.
            self.dialog_with_retry(trigger = lambda: self.content_tab_info_action(1, 2, "Options"),
                                   expect = { "crypto_extra_options": CheckBoxText("crypto,options") },
                                   values = { "crypto_extra_options": CheckBoxText("weird,options") })

            assert m.execute("grep 'weird,options' /etc/crypttab") != ""
            self.wait_in_storaged_configuration("weird,options")

            # Change passphrase
            edit_button = self.content_tab_info_row(1, 2, "Stored passphrase") + " button"
            self.dialog_with_retry(trigger = lambda: b.click(edit_button),
                                   expect = { "passphrase": "vainu-reku-toma-rolle-kaja" },
                                   values = { "passphrase": "wrong-passphrase" })

            assert m.execute("cat /etc/luks-keys/*") == "wrong-passphrase"

            # Remove passphrase
            edit_button = self.content_tab_info_row(1, 2, "Stored passphrase") + " button"
            self.dialog_with_retry(trigger = lambda: b.click(edit_button),
                                   expect = { "passphrase": "wrong-passphrase" },
                                   values = { "passphrase": "" })
            self.wait_in_storaged_configuration("'passphrase-path': <b''>")

            # Lock it
            self.content_head_action(1, "Lock")
            b.wait_not_in_text("#detail-content", "ext4 File System")

            # Unlock, this asks for a passphrase
            self.content_head_action(1, "Unlock")
            self.dialog({ "passphrase": "vainu-reku-toma-rolle-kaja" })
            self.content_row_wait_in_col(2, 1, "ext4 File System")

            # Delete the partition.
            self.content_head_action(1, "Delete")
            self.confirm()
            self.content_row_wait_in_col(1, 1, "Free Space")
            b.wait_not_in_text("#detail-content", "ext4 File System")

        else:

            # Unlock, this asks for a passphrase because we don't store one with older UDisks2.
            self.content_head_action(1, "Unlock")
            self.dialog({ "passphrase": "vainu-reku-toma-rolle-kaja" })
            self.content_row_wait_in_col(2, 1, "ext4 File System")

        assert m.execute("grep -v ^# /etc/crypttab || true").strip() == ""
        assert m.execute("grep %s /etc/fstab || true" % mount_point_secret) == ""

    def testClevisTang(self):
        m = self.machine
        b = self.browser

        if m.execute("which clevis || true") == "":
            self.skipTest("No clevis/tang here")

        # We generate the keys and cache explicitly before starting
        # the socket.  This allows us to control their names, which
        # helps later on.

        # HACK - tangd-udpate.service can't create /var/cache/tang.
        #
        #        https://github.com/latchset/tang/issues/24

        m.execute("mkdir /var/cache/tang; chown -R tang:tang /var/cache/tang")

        m.execute("rm -rf /var/db/tang/*")
        m.execute("jose jwk gen -i '{\"alg\":\"ES512\"}' >/var/db/tang/sig1.jwk")
        m.execute("jose jwk gen -i '{\"alg\":\"ECMR\"}' -o /var/db/tang/enc1.jwk")

        # HACK - /var/cache/tang is automatically updated whenever
        #        /var/db/tang changes, but also asynchronously in the
        #        background, and it's not reliable either.
        #
        #        https://github.com/latchset/tang/issues/23

        m.execute("systemctl reset-failed tangd-update; systemctl restart tangd-update")

        m.execute("systemctl start tangd.socket")

        self.login_and_go("/storage")

        # Add a disk and format it with luks
        m.add_disk("50M", serial="MYDISK")
        b.wait_in_text("#drives", "MYDISK")
        b.click('tr:contains("MYDISK")')
        b.wait_visible("#storage-detail")

        self.content_tab_action(1, 1, "Format")
        self.dialog({ "type": "luks+ext4",
                      "name": "ENCRYPTED",
                      "passphrase": "vainu-reku-toma-rolle-kaja",
                      "passphrase2": "vainu-reku-toma-rolle-kaja" })
        self.content_row_wait_in_col(1, 1, "Encrypted data")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        # Set the flag and reload the page to make the Clevis stuff appear
        #
        b.eval_js('window.localStorage["clevis-feature"] = "on"')
        b.reload()
        b.enter_page("/storage")

        # Lock the disk
        #
        self.content_head_action(1, "Lock")
        b.wait_not_in_text("#detail-content", "ext4 File System")

        # Add a key
        #
        self.content_tab_wait_in_info(1, 1, "Network keys", "")
        self.content_tab_action(1, 1, "Add")
        self.dialog_wait_open()
        self.dialog_set_val("method", "tang")
        self.dialog_set_val("tang_url", "127.0.0.1")
        self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
        self.dialog_apply()
        b.wait_in_text("#dialog", "The output should match this text")
        b.wait_in_text("#dialog", m.execute("jose jwk thp -i /var/db/tang/sig1.jwk").strip())

        self.dialog_apply()
        self.dialog_wait_close()
        self.content_tab_wait_in_info(1, 1, "Network keys", "127.0.0.1")

        # Unlock it.  This should succeed without prompting.
        #
        self.content_head_action(1, "Unlock")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        # Rotate the encryption key on the server, and rebind

        m.execute("mv /var/db/tang/enc1.jwk /var/db/tang/.enc1.jwk")
        m.execute("jose jwk gen -i '{\"alg\":\"ECMR\"}' >/var/db/tang/enc2.jwk")
        m.execute("systemctl reset-failed tangd-update; systemctl restart tangd-update")

        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "This network key is obsolete.")
        b.wait_in_text("#dialog", "A new key has been securely retrieved from the server.")
        self.dialog_apply()
        self.dialog_wait_close()

        # Remove obsolete key, disk should still unlock

        m.execute("rm /var/db/tang/.enc1.jwk")
        m.execute("systemctl restart tangd-update")
        self.content_head_action(1, "Lock")
        b.wait_not_in_text("#detail-content", "ext4 File System")
        self.content_head_action(1, "Unlock")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        # Rotate both the encryption key and the signing key on the server, and rebind

        m.execute("mv /var/db/tang/enc2.jwk /var/db/tang/.enc2.jwk")
        m.execute("jose jwk gen -i '{\"alg\":\"ECMR\"}' >/var/db/tang/enc3.jwk")
        m.execute("mv /var/db/tang/sig1.jwk /var/db/tang/.sig1.jwk")
        m.execute("jose jwk gen -i '{\"alg\":\"ES512\"}' >/var/db/tang/sig2.jwk")
        m.execute("systemctl reset-failed tangd-update; systemctl restart tangd-update")

        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "This network key is obsolete.")
        b.wait_in_text("#dialog", "The output should match this text")
        b.wait_in_text("#dialog", m.execute("jose jwk thp -i /var/db/tang/sig2.jwk").strip())
        self.dialog_apply()
        self.dialog_wait_close()

        # Remove the key on the server

        m.execute("rm /var/db/tang/enc3.jwk")
        m.execute("systemctl reset-failed tangd-update; systemctl restart tangd-update")
        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "This network key is not recognized anymore by the server.")
        self.dialog_cancel()
        self.dialog_wait_close()

        # Remove key on client

        b.wait_in_text(self.content_tab_info_row(1, 1, "Network keys"), "127.0.0.1")
        self.content_tab_action(1, 1, "Remove")
        self.confirm()
        b.wait_not_in_text(self.content_tab_info_row(1, 1, "Network keys"), "127.0.0.1")

    def testClevisHttp(self):
        m = self.machine
        b = self.browser

        if m.execute("which clevis || true") == "":
            self.skipTest("No clevis/tang here")

        # Start our escrow server
        m.upload([ "verify/files/mock-escrow-server" ], "/usr/local/bin/")
        escrow_pid = m.spawn("/usr/local/bin/mock-escrow-server /tmp/SECRET 88", "mock-escrow-server.log")

        self.login_and_go("/storage")

        # Add a disk and format it with luks
        m.add_disk("50M", serial="MYDISK")
        b.wait_in_text("#drives", "MYDISK")
        b.click('tr:contains("MYDISK")')
        b.wait_visible("#storage-detail")

        self.content_tab_action(1, 1, "Format")
        self.dialog({ "type": "luks+ext4",
                      "name": "ENCRYPTED",
                      "passphrase": "vainu-reku-toma-rolle-kaja",
                      "passphrase2": "vainu-reku-toma-rolle-kaja" })
        self.content_row_wait_in_col(1, 1, "Encrypted data")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        # Set the flag and reload the page to make the Clevis stuff appear
        #
        b.eval_js('window.localStorage["clevis-feature"] = "on"')
        b.reload()
        b.enter_page("/storage")

        # Lock the disk
        #
        self.content_head_action(1, "Lock")
        b.wait_not_in_text("#detail-content", "ext4 File System")

        # Add a key
        #
        self.content_tab_wait_in_info(1, 1, "Network keys", "")
        self.content_tab_action(1, 1, "Add")
        self.dialog_wait_open()
        self.dialog_set_val("method", "http")
        self.dialog_set_val("http_url", "http://127.0.0.1:88")
        self.dialog_set_val("allow_plain_http", True)
        self.dialog_set_val("http_method", "PUT")
        self.dialog_set_val("key_type", "octet-stream")
        self.dialog_set_val("passphrase", "vainu-reku-toma-rolle-kaja")
        self.dialog_apply()
        self.dialog_wait_close()
        self.content_tab_wait_in_info(1, 1, "Network keys", "http://127.0.0.1:88")

        # Unlock it.  This should succeed without prompting.
        #
        self.content_head_action(1, "Unlock")
        self.content_row_wait_in_col(2, 1, "ext4 File System")

        # Check the key, should be working right now
        #
        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "This network key works fine right now")
        self.dialog_cancel()
        self.dialog_wait_close()

        # Break the key in the server and check again
        #
        m.execute("echo foo >/tmp/SECRET")
        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "The server has returned a key that doesn't work.")
        self.dialog_cancel()
        self.dialog_wait_close()

        # Kill the server and check
        #
        m.execute("kill %s" % escrow_pid)
        self.content_tab_action(1, 1, "Check")
        self.dialog_wait_open()
        b.wait_in_text("#dialog", "Retrieving the key from http://127.0.0.1:88 has failed: Connection refused.")
        self.dialog_cancel()
        self.dialog_wait_close()

        # Remove key on client
        #
        self.content_tab_action(1, 1, "Remove")
        self.confirm()
        b.wait_not_in_text(self.content_tab_info_row(1, 1, "Network keys"), "http://127.0.0.1:88")

if __name__ == '__main__':
    test_main()
