# tripledeskw.py - implementation of Triple DES key wrapping
# coding: utf-8
#
# Copyright (C) 2014-2025 Arthur de Jong
#
# This library 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.
#
# This library 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 this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA

"""Implement Triple DES key wrapping as described in RFC 3217."""

from __future__ import annotations

import binascii
import hashlib
import os

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

from pskc.exceptions import DecryptionError, EncryptionError


def _cms_hash(value: bytes) -> bytes:
    """Return the key hash algorithm described in RFC 3217 section 2."""
    return hashlib.sha1(value).digest()[:8]


RFC3217_IV = binascii.a2b_hex('4adda22c79e82105')


def wrap(plaintext: bytes, key: bytes, iv: bytes | None = None) -> bytes:
    """Wrap one key (typically a Triple DES key) with another Triple DES key.

    This uses the algorithm from RFC 3217 to encrypt the plaintext (the key
    to wrap) using the provided key. If the iv is None, it is randomly
    generated.
    """
    if 8 * len(plaintext) % algorithms.TripleDES.block_size != 0:
        raise EncryptionError('Plaintext length wrong')
    if iv is None:
        iv = os.urandom(8)
    backend = default_backend()
    cipher = Cipher(algorithms.TripleDES(key), modes.CBC(iv), backend)
    encryptor = cipher.encryptor()
    tmp = (
        iv + encryptor.update(plaintext + _cms_hash(plaintext)) +
        encryptor.finalize())
    cipher = Cipher(algorithms.TripleDES(key), modes.CBC(RFC3217_IV), backend)
    encryptor = cipher.encryptor()
    return encryptor.update(tmp[::-1]) + encryptor.finalize()


def unwrap(ciphertext: bytes, key: bytes) -> bytes:
    """Unwrap a key (typically Triple DES key ) with another Triple DES key.

    This uses the algorithm from RFC 3217 to decrypt the ciphertext (the
    previously wrapped key) using the provided key.
    """
    if 8 * len(ciphertext) % algorithms.TripleDES.block_size != 0:
        raise DecryptionError('Ciphertext length wrong')
    backend = default_backend()
    cipher = Cipher(algorithms.TripleDES(key), modes.CBC(RFC3217_IV), backend)
    decryptor = cipher.decryptor()
    tmp = (decryptor.update(ciphertext) + decryptor.finalize())[::-1]
    cipher = Cipher(algorithms.TripleDES(key), modes.CBC(tmp[:8]), backend)
    decryptor = cipher.decryptor()
    tmp = decryptor.update(tmp[8:]) + decryptor.finalize()
    if tmp[-8:] == _cms_hash(tmp[:-8]):
        return tmp[:-8]
    raise DecryptionError('CMS key checksum error')
