/*
 * Copyright (C) 2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "PushMessageCrypto.h"

#if ENABLE(SERVICE_WORKER)

#include "PushCrypto.h"
#include <wtf/ByteOrder.h>
#include <wtf/CryptographicallyRandomNumber.h>

namespace WebCore::PushCrypto {

// Arbitrary limit that's larger than the largest payload APNS should ever give us.
static constexpr size_t maxPushPayloadLength = 65535;

// From RFC8291.
static constexpr size_t saltLength = 16;
static constexpr size_t sharedAuthSecretLength = 16;

ClientKeys ClientKeys::generate()
{
    uint8_t sharedAuthSecret[sharedAuthSecretLength];
    cryptographicallyRandomValues(sharedAuthSecret, sizeof(sharedAuthSecret));

    return ClientKeys {
        P256DHKeyPair::generate(),
        Vector<uint8_t> { sharedAuthSecret, sizeof(sharedAuthSecret) }
    };
}

static bool areClientKeyLengthsValid(const ClientKeys& clientKeys)
{
    return clientKeys.clientP256DHKeyPair.publicKey.size() == p256dhPublicKeyLength && clientKeys.clientP256DHKeyPair.privateKey.size() == p256dhPrivateKeyLength && clientKeys.sharedAuthSecret.size() == sharedAuthSecretLength;
}

static size_t computeAES128GCMPaddingLength(const uint8_t *begin, size_t length)
{
    /*
     * Compute padding length as defined in RFC8188 Section 2:
     *
     *   +-----------+-----+
     *   |   data    | pad |
     *   +-----------+-----+
     *
     * pad must be of non-zero length and is a delimiter octet (0x02) followed by any number of 0x00 octets.
     */
    if (!length)
        return SIZE_MAX;

    const uint8_t* end = begin + length;
    const uint8_t* cur = end - 1;
    while (cur > begin && (*cur == 0x00))
        --cur;
    if (*cur != 0x02)
        return SIZE_MAX;

    return end - cur;
}

std::optional<Vector<uint8_t>> decryptAES128GCMPayload(const ClientKeys& clientKeys, Span<const uint8_t> payload)
{
    if (!areClientKeyLengthsValid(clientKeys))
        return std::nullopt;

    // Extract encryption parameters from header as described in RFC8188.
    struct PayloadHeader {
        uint8_t salt[saltLength];
        uint8_t ignored[4];
        uint8_t keyLength;
        uint8_t serverPublicKey[p256dhPublicKeyLength];
    };
    static_assert(sizeof(PayloadHeader) == 86);
    static constexpr size_t minPushPayloadLength = sizeof(PayloadHeader) + 1 /* minPaddingLength */ + aes128GCMTagLength;

    if (payload.size() < minPushPayloadLength || payload.size() > maxPushPayloadLength)
        return std::nullopt;

    PayloadHeader header;
    memcpy(&header, payload.data(), sizeof(header));

    if (header.keyLength != p256dhPublicKeyLength)
        return std::nullopt;

    /*
     * The rest of the comments are snippets from RFC8291 3.4.
     *
     * -- For a user agent:
     * ecdh_secret = ECDH(ua_private, as_public)
     */
    auto ecdhSecretResult = computeP256DHSharedSecret(header.serverPublicKey, clientKeys.clientP256DHKeyPair);
    if (!ecdhSecretResult)
        return std::nullopt;

    /*
     * # HKDF-Extract(salt=auth_secret, IKM=ecdh_secret)
     * PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)
     */
    auto prkKey = hmacSHA256(clientKeys.sharedAuthSecret, *ecdhSecretResult);

    /*
     * # HKDF-Expand(PRK_key, key_info, L_key=32)
     * key_info = "WebPush: info" || 0x00 || ua_public || as_public
     * IKM = HMAC-SHA-256(PRK_key, key_info || 0x01)
     */
    struct KeyInfo {
        char label[14] = { "WebPush: info" };
        uint8_t clientKey[p256dhPublicKeyLength];
        uint8_t serverKey[p256dhPublicKeyLength];
        uint8_t end = 0x01;
    };
    static_assert(sizeof(KeyInfo) == 145);

    KeyInfo keyInfo;
    memcpy(keyInfo.clientKey, clientKeys.clientP256DHKeyPair.publicKey.data(), p256dhPublicKeyLength);
    memcpy(keyInfo.serverKey, header.serverPublicKey, p256dhPublicKeyLength);

    auto ikm = hmacSHA256(prkKey, Span(reinterpret_cast<uint8_t*>(&keyInfo), sizeof(keyInfo)));

    /*
     * # HKDF-Extract(salt, IKM)
     * PRK = HMAC-SHA-256(salt, IKM)
     */
    auto prk = hmacSHA256(header.salt, ikm);

    /*
     * # HKDF-Expand(PRK, cek_info, L_cek=16)
     * cek_info = "Content-Encoding: aes128gcm" || 0x00
     * CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]
     */
    static const uint8_t cekInfo[] = "Content-Encoding: aes128gcm\x00\x01";
    auto cek = hmacSHA256(prk, Span(cekInfo, sizeof(cekInfo) - 1));
    cek.shrink(16);

    /*
     * # HKDF-Expand(PRK, nonce_info, L_nonce=12)
     * nonce_info = "Content-Encoding: nonce" || 0x00
     * NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]
     */
    static const uint8_t nonceInfo[] = "Content-Encoding: nonce\x00\x01";
    auto nonce = hmacSHA256(prk, Span(nonceInfo, sizeof(nonceInfo) - 1));
    nonce.shrink(12);

    // Finally, decrypt with AES128GCM and return the unpadded plaintext.
    auto cipherText = Span(payload.data() + sizeof(header), payload.size() - sizeof(header));
    auto plainTextResult = decryptAES128GCM(cek, nonce, cipherText);
    if (!plainTextResult)
        return std::nullopt;

    auto plainText = WTFMove(plainTextResult.value());
    size_t paddingLength = computeAES128GCMPaddingLength(plainText.data(), plainText.size());
    if (paddingLength == SIZE_MAX)
        return std::nullopt;

    plainText.shrink(plainText.size() - paddingLength);
    return plainText;
}

static size_t computeAESGCMPaddingLength(const uint8_t *begin, size_t length)
{
    /*
     * Compute padding length as defined in draft-ietf-httpbis-encryption-encoding-03:
     *
     *   +-----+-----------+
     *   | pad |   data    |
     *   +-----+-----------+
     *
     * Padding consists of a two octet unsigned integer in network byte order, followed by that
     * number of 0x00 octets. The minimum padding size is 2 bytes.
     */
    if (length < 2)
        return SIZE_MAX;

    uint16_t paddingLength;
    memcpy(&paddingLength, begin, 2);
    paddingLength = ntohs(paddingLength);

    const uint8_t* cur = begin + 2;
    const uint8_t* end = begin + length;
    uint16_t paddingLeft = paddingLength;
    while (cur < end && (*cur == 0x0) && paddingLeft) {
        ++cur;
        --paddingLeft;
    }

    if (paddingLeft)
        return SIZE_MAX;

    return cur - begin;
}

std::optional<Vector<uint8_t>> decryptAESGCMPayload(const ClientKeys& clientKeys, Span<const uint8_t> serverP256DHPublicKey, Span<const uint8_t> salt, Span<const uint8_t> payload)
{
    if (!areClientKeyLengthsValid(clientKeys) || serverP256DHPublicKey.size() != p256dhPublicKeyLength || salt.size() != saltLength)
        return std::nullopt;

    // Padding must be at least the size of the two octet unsigned integer used in the padding scheme plus the size of the AES128GCM tag.
    if (payload.size() < 2 + aes128GCMTagLength || payload.size() > maxPushPayloadLength)
        return std::nullopt;

    /*
     * These comments are snippets from draft-ietf-webpush-encryption-04.
     *
     * -- For a User Agent:
     * ecdh_secret = ECDH(ua_private, as_public)
     */
    auto ecdhSecretResult = computeP256DHSharedSecret(serverP256DHPublicKey, clientKeys.clientP256DHKeyPair);
    if (!ecdhSecretResult)
        return std::nullopt;

    /*
     * auth_info = "Content-Encoding: auth" || 0x00
     * PRK_combine = HMAC-SHA-256(auth_secret, ecdh_secret)
     * IKM = HMAC-SHA-256(PRK_combine, auth_info || 0x01)
     * PRK = HMAC-SHA-256(salt, IKM)
     */
    static const uint8_t authInfo[] = "Content-Encoding: auth\x00\x01";
    auto prkCombine = hmacSHA256(clientKeys.sharedAuthSecret, *ecdhSecretResult);
    auto ikm = hmacSHA256(prkCombine, Span(authInfo, sizeof(authInfo) - 1));
    auto prk = hmacSHA256(salt, ikm);

    /*
     * context = "P-256" || 0x00 ||
     *           0x00 || 0x41 || ua_public ||
     *           0x00 || 0x41 || as_public
     *
     * Note that we also append a 0x01 byte at the end here since the cek and nonce
     * derivation functions below require that trailing 0x01 byte.
     */
    struct KeyDerivationContext {
        char label[6] = { "P-256" };
        uint8_t clientPublicKeyLength[2] = { 0, 0x41 };
        uint8_t clientPublicKey[p256dhPublicKeyLength];
        uint8_t serverPublicKeyLength[2] = { 0, 0x41 };
        uint8_t serverPublicKey[p256dhPublicKeyLength];
        uint8_t end = 0x01;
    };
    static_assert(sizeof(KeyDerivationContext) == 141);
    KeyDerivationContext context;
    memcpy(context.clientPublicKey, clientKeys.clientP256DHKeyPair.publicKey.data(), p256dhPublicKeyLength);
    memcpy(context.serverPublicKey, serverP256DHPublicKey.data(), p256dhPublicKeyLength);

    /*
     * cek_info = "Content-Encoding: aesgcm" || 0x00 || context
     * CEK = HMAC-SHA-256(PRK, cek_info || 0x01)[0..15]
     */
    static const uint8_t cekInfoHeader[] = "Content-Encoding: aesgcm";
    uint8_t cekInfo[sizeof(cekInfoHeader) + sizeof(context)];
    memcpy(cekInfo, cekInfoHeader, sizeof(cekInfoHeader));
    memcpy(cekInfo + sizeof(cekInfoHeader), &context, sizeof(context));

    auto cek = hmacSHA256(prk, cekInfo);
    cek.shrink(16);

    /*
     * nonce_info = "Content-Encoding: nonce" || 0x00 || context
     * NONCE = HMAC-SHA-256(PRK, nonce_info || 0x01)[0..11]
     */
    static const uint8_t nonceInfoHeader[] = "Content-Encoding: nonce";
    uint8_t nonceInfo[sizeof(nonceInfoHeader) + sizeof(context)];
    memcpy(nonceInfo, nonceInfoHeader, sizeof(nonceInfoHeader));
    memcpy(nonceInfo + sizeof(nonceInfoHeader), &context, sizeof(context));

    auto nonce = hmacSHA256(prk, nonceInfo);
    nonce.shrink(12);

    // Finally, decrypt with AES128GCM and return the unpadded plaintext.
    auto plainTextResult = decryptAES128GCM(cek, nonce, payload);
    if (!plainTextResult)
        return std::nullopt;

    auto plainText = WTFMove(plainTextResult.value());
    size_t paddingLength = computeAESGCMPaddingLength(plainText.data(), plainText.size());
    if (paddingLength == SIZE_MAX)
        return std::nullopt;

    return Vector<uint8_t> { plainText.data() + paddingLength, plainText.size() - paddingLength };
}

} // namespace WebCore::PushCrypto

#endif // ENABLE(SERVICE_WORKER)
