All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micronaut.http.server.netty.QuicTokenHandlerImpl Maven / Gradle / Ivy

There is a newer version: 4.7.9
Show newest version
/*
 * Copyright 2017-2023 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.http.server.netty;

import io.micronaut.core.annotation.Internal;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.incubator.codec.quic.QuicTokenHandler;
import io.netty.util.concurrent.FastThreadLocal;

import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import java.net.InetSocketAddress;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * Secure {@link QuicTokenHandler} implementation based on a MAC.
 *
 * @since 4.0.0
 * @author Jonas Konrad
 */
@Internal
class QuicTokenHandlerImpl implements QuicTokenHandler {
    private static final int MAC_LENGTH = 256 / 8;
    private static final int MAX_CONNECTION_ID_LENGTH = 20;
    /**
     * Timestamp will be modulo'd by this window size, and included in the MAC. In verification, we
     * check the last two windows, so a given token expires in roughly this time frame.
     */
    private static final long TIMESTAMP_WINDOW_SIZE = 5 * 60 * 1000;

    private final Key key;
    /**
     * Making this non-static is not ideal, but it allows initializing the mac with the key only
     * once.
     */
    private final FastThreadLocal macCache = new FastThreadLocal<>() {
        @Override
        protected Mac initialValue() throws Exception {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(key);
            return mac;
        }
    };

    private final ByteBufAllocator alloc;

    QuicTokenHandlerImpl(ByteBufAllocator alloc) {
        this.alloc = alloc;
        try {
            key = KeyGenerator.getInstance("HmacSHA256").generateKey();
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(e);
        }
    }

    /**
     * Alias for {@link #QuicTokenHandlerImpl}, avoids a {@link NoClassDefFoundError} when quic is
     * missing from the classpath.
     */
    static QuicTokenHandler create(ByteBufAllocator alloc) {
        return new QuicTokenHandlerImpl(alloc);
    }

    /**
     * Write the validation token. The output buffer contains first the token, then the
     * {@code dcid}, the same format that is passed into {@link #validateToken}.
     *
     * @param out       {@link ByteBuf} into which the token will be written.
     * @param dcid      the destination connection id.
     * @param address   the {@link InetSocketAddress} of the sender.
     * @return {@code true}
     */
    @Override
    public boolean writeToken(ByteBuf out, ByteBuf dcid, InetSocketAddress address) {
        byte[] hash = hash(address, dcid, currentWindowId());
        out.writeBytes(hash);
        out.writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes());
        return true;
    }

    /**
     * Verify the validation token. The input buffer is user-controlled, but should contain the
     * same format returned by {@link #writeToken} (first token, then dcid). This method extracts
     * the dcid from the input, computes the MAC, and validates it against the token.
     *
     * @param token     the {@link ByteBuf} that contains the token. The ownership is not transferred.
     * @param address   the {@link InetSocketAddress} of the sender.
     * @return The offset of the dcid in the input buffer. This is used by netty. -1 on validation
     * failure.
     */
    @Override
    public int validateToken(ByteBuf token, InetSocketAddress address) {
        byte[] actual = new byte[MAC_LENGTH];
        token.getBytes(token.readerIndex(), actual);
        ByteBuf dcid = token.slice(token.readerIndex() + MAC_LENGTH, token.readableBytes() - MAC_LENGTH);

        long windowId = currentWindowId();
        byte[] expectedHashNow = hash(address, dcid, windowId);
        byte[] expectedHashPrev = hash(address, dcid, windowId - 1);
        // constant-time comparison
        boolean equalNow = MessageDigest.isEqual(expectedHashNow, actual);
        boolean equalPrev = MessageDigest.isEqual(expectedHashPrev, actual);
        if (equalNow | equalPrev) {
            return MAC_LENGTH;
        } else {
            return -1;
        }
    }

    private byte[] hash(InetSocketAddress address, ByteBuf dcid, long windowId) {
        ByteBuf textToVerify = buildTextToVerify(address, dcid, windowId);
        byte[] hash;
        try {
            Mac mac = macCache.get();
            mac.update(textToVerify.array(), textToVerify.arrayOffset() + textToVerify.readerIndex(), textToVerify.readableBytes());
            hash = mac.doFinal();
        } finally {
            textToVerify.release();
        }
        assert hash.length == MAC_LENGTH;
        return hash;
    }

    private ByteBuf buildTextToVerify(InetSocketAddress address, ByteBuf dcid, long windowId) {
        if (dcid.readableBytes() > MAX_CONNECTION_ID_LENGTH) {
            throw new IllegalArgumentException("Connection ID may not exceed 20 bytes length");
        }
        ByteBuf textToVerify = alloc.heapBuffer();
        byte[] addressBytes = address.getAddress().getAddress();
        textToVerify.writeByte(addressBytes.length);
        textToVerify.writeBytes(addressBytes);
        textToVerify.writeShort(address.getPort());
        textToVerify.writeByte(dcid.readableBytes());
        textToVerify.writeBytes(dcid, dcid.readerIndex(), dcid.readableBytes());
        textToVerify.writeLong(windowId);
        return textToVerify;
    }

    @Override
    public int maxTokenLength() {
        return MAC_LENGTH + MAX_CONNECTION_ID_LENGTH;
    }

    // overridden in test
    long currentWindowId() {
        return System.currentTimeMillis() / TIMESTAMP_WINDOW_SIZE;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy