Last active
February 20, 2020 16:45
-
-
Save jrudolph/680eb6ea93dcba6a1f07021949596975 to your computer and use it in GitHub Desktop.
"Simple" "Http/3" "client"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.math.BigInteger | |
import java.net.DatagramPacket | |
import java.net.DatagramSocket | |
import java.net.InetAddress | |
import java.security.KeyPairGenerator | |
import java.security.MessageDigest | |
import java.security.SecureRandom | |
import java.security.interfaces.ECPrivateKey | |
import java.security.interfaces.ECPublicKey | |
import java.security.spec.ECPoint | |
import java.util.Random | |
import Helpers._ | |
import akka.util.ByteString | |
import akka.util.ByteStringBuilder | |
import javax.crypto.Cipher | |
import javax.crypto.KeyAgreement | |
import javax.crypto.Mac | |
import javax.crypto.spec.GCMParameterSpec | |
import javax.crypto.spec.SecretKeySpec | |
import sun.security.ec.ECPublicKeyImpl | |
import scala.annotation.tailrec | |
import scala.util.control.NonFatal | |
object SimpleQuicClientMain extends App { | |
val random = new SecureRandom | |
def uint16BE(i: Int): ByteString = | |
ByteString(i >> 8, i & 0xff) | |
def uint24BE(i: Int): ByteString = | |
ByteString( | |
i >> 16, | |
i >> 8 & 0xff, | |
i & 0xff) | |
val ecdheKeys = KeyPairGenerator.getInstance("EC") | |
val pair = ecdheKeys.generateKeyPair() | |
println("Private: " + pair.getPrivate.getEncoded.hexString) | |
println("Public: " + pair.getPublic.getEncoded.hexString) | |
val srcConnId = random.nextByteString(8) | |
println("Src Conn Id: " + srcConnId.toArray.hexString) | |
var dstConnId = random.nextByteString(8) | |
println("Dest Conn Id: " + dstConnId.toArray.hexString) | |
val originalDstConnId = dstConnId | |
val token = ByteString.empty //"71 75 69 63 68 65 00 00 00 00 00 00 00 00 00 00 FF FF 5D DA 23 13 2C 2C 67 BD F1 87 1C A9".hexByteString | |
val initialSalt = "c3eef712c72ebb5a11a7d2432bb46365bef9f502".hexByteArray | |
val initialSecret = hkdf.extract(initialSalt, dstConnId.toArray) | |
val clientInitialSecret = hkdfExpandLabel(initialSecret, "client in", Array.empty, 32) | |
val clientKey = hkdfExpandLabel(clientInitialSecret, "quic key", Array.empty, 16) | |
val clientIv = hkdfExpandLabel(clientInitialSecret, "quic iv", Array.empty, 12) | |
val clientHp = hkdfExpandLabel(clientInitialSecret, "quic hp", Array.empty, 16) | |
val serverInitialSecret = hkdfExpandLabel(initialSecret, "server in", Array.empty, 32) | |
val serverKey = hkdfExpandLabel(serverInitialSecret, "quic key", Array.empty, 16) | |
val serverIv = hkdfExpandLabel(serverInitialSecret, "quic iv", Array.empty, 12) | |
val serverHp = hkdfExpandLabel(serverInitialSecret, "quic hp", Array.empty, 16) | |
val payloadCipher = Cipher.getInstance("AES/GCM/NoPadding") | |
val hpCipher = Cipher.getInstance("AES/ECB/NoPadding") | |
var transcript: ByteString = ByteString.empty | |
def addToTranscript(handshakeMessage: ByteString): Unit = transcript ++= handshakeMessage | |
def currentTranscriptHash(): Array[Byte] = { | |
val digest = MessageDigest.getInstance("SHA-256") | |
digest.digest((transcript).toArray) | |
} | |
var handshakeSecret: Array[Byte] = _ | |
var serverHandshakeSecret: Array[Byte] = _ | |
var clientHandshakeSecret: Array[Byte] = _ | |
var masterSecret: Array[Byte] = _ | |
var transcriptUntilCertificateVerify: Array[Byte] = _ | |
var finishedKey: Array[Byte] = _ | |
var clientApplicationSecret: Array[Byte] = _ | |
var serverApplicationSecret: Array[Byte] = _ | |
//val target = InetAddress.getByName("localhost") | |
//val targetPort = 4443 | |
val target = InetAddress.getByName("quic.rocks") | |
val targetPort = 4433 | |
val socket = new DatagramSocket(12346) | |
socket.setSoTimeout(5000) | |
def send(packet: ByteString): Unit = | |
socket.send(new DatagramPacket(packet.toArray, packet.size, target, targetPort)) | |
def createCRYPTO(payload: ByteString): ByteString = | |
"06 00".hexByteString ++ | |
uint16BE(payload.length + 0x4000) ++ // assuming length < 14 bits | |
payload // works only for first packet because offset is fixed to 0 | |
def handshakeMessage(msgType: Int, payload: ByteString): ByteString = | |
ByteString(msgType.toByte) ++ uint24BE(payload.length) ++ payload | |
var initialPacketNumber = 0 | |
def createInitialPacket(payload0: ByteString): ByteString = { | |
println("Payload: " + payload0.toArray.hexString) | |
initialPacketNumber += 1 | |
val packetNumber = initialPacketNumber | |
require(packetNumber < 32) | |
val packetNumberLength = 1 // as long as fits into a byte | |
val padding = math.max(0, 1270 - payload0.size) | |
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte)) | |
val payloadLength = { | |
val len = packetNumberLength + payload.length + 16 // AEAD signature | |
uint16BE(len + 0x4000) // assuming length is < 14 bits | |
} | |
val plainHeader = | |
"C0 FF 00 00 17".hexByteString ++ | |
ByteString(dstConnId.size.toByte) ++ dstConnId ++ | |
ByteString(srcConnId.size.toByte) ++ srcConnId ++ | |
ByteString(token.size.toByte) ++ token ++ | |
payloadLength ++ | |
ByteString(packetNumber.toByte) | |
println("Plain header: " + plainHeader.toArray.hexString) | |
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */ | |
val nonce = clientIv.clone() | |
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers | |
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader.toArray) | |
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray)) | |
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1 | |
println(s"Sample: ${sample.toArray.hexString}") | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES")) | |
val mask = hpCipher.doFinal(sample.toArray) | |
println(s"Mask: ${mask.hexString}") | |
val protectedHeader = plainHeader.toArray | |
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x0f)).toByte | |
val packetNumberOffset = 10 + dstConnId.size + srcConnId.size + token.size | |
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte | |
ByteString(protectedHeader ++ encryptedPayload) | |
} | |
var handshakePacketNumber = 0 | |
def createHandshakePacket(payload0: ByteString): ByteString = { | |
println("Payload: " + payload0.toArray.hexString) | |
handshakePacketNumber += 1 | |
val packetNumber = handshakePacketNumber | |
require(packetNumber < 200) | |
val packetNumberLength = 1 // as long as fits into a byte | |
val padding = math.max(0, 1270 - payload0.size) | |
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte)) | |
val payloadLength = { | |
val len = packetNumberLength + payload.length + 16 // AEAD signature | |
uint16BE(len + 0x4000) // assuming length is < 14 bits | |
} | |
val plainHeader = | |
"E0 FF 00 00 17".hexByteString ++ | |
ByteString(dstConnId.size.toByte) ++ dstConnId ++ | |
ByteString(srcConnId.size.toByte) ++ srcConnId ++ | |
payloadLength ++ | |
ByteString(packetNumber.toByte) | |
println("Plain header: " + plainHeader.toArray.hexString) | |
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */ | |
val clientKey = hkdfExpandLabel(clientHandshakeSecret, "quic key", Array.empty, 16) | |
val clientIv = hkdfExpandLabel(clientHandshakeSecret, "quic iv", Array.empty, 12) | |
val clientHp = hkdfExpandLabel(clientHandshakeSecret, "quic hp", Array.empty, 16) | |
val nonce = clientIv.clone() | |
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers | |
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader.toArray) | |
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray)) | |
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1 | |
println(s"Sample: ${sample.toArray.hexString}") | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES")) | |
val mask = hpCipher.doFinal(sample.toArray) | |
println(s"Mask: ${mask.hexString}") | |
val protectedHeader = plainHeader.toArray | |
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x0f)).toByte | |
val packetNumberOffset = 9 + dstConnId.size + srcConnId.size | |
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte | |
ByteString(protectedHeader ++ encryptedPayload) | |
} | |
var shortPacketNumber = 0 | |
def createShortPacket(payload0: ByteString): ByteString = { | |
println("Payload: " + payload0.toArray.hexString) | |
shortPacketNumber += 1 | |
val packetNumber = shortPacketNumber | |
require(packetNumber < 200) | |
val packetNumberLength = 1 // as long as fits into a byte | |
val padding = math.max(0, 500 - payload0.size) | |
val payload = payload0 ++ ByteString(Array.fill(padding)(0.toByte)) | |
val plainHeader = | |
"40".hexByteString ++ // keeping spin and key phase to 0, pn_length = 0 | |
dstConnId ++ | |
ByteString(packetNumber.toByte) | |
println("Plain header: " + plainHeader.toArray.hexString) | |
//val prelimSize = plainHeader.size + payload.size + 16 /* AEAD signature */ | |
val clientKey = hkdfExpandLabel(clientApplicationSecret, "quic key", Array.empty, 16) | |
val clientIv = hkdfExpandLabel(clientApplicationSecret, "quic iv", Array.empty, 12) | |
val clientHp = hkdfExpandLabel(clientApplicationSecret, "quic hp", Array.empty, 16) | |
val nonce = clientIv.clone() | |
nonce(11) = (nonce(11) ^ packetNumber).toByte // works only with single byte packet numbers | |
payloadCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader.toArray) | |
val encryptedPayload = ByteString(payloadCipher.doFinal(payload.toArray)) | |
val sample = encryptedPayload.drop(3).take(16) // assuming packet number length == 1 | |
println(s"Sample: ${sample.toArray.hexString}") | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(clientHp, "AES")) | |
val mask = hpCipher.doFinal(sample.toArray) | |
println(s"Mask: ${mask.hexString}") | |
val protectedHeader = plainHeader.toArray | |
protectedHeader(0) = (protectedHeader(0) ^ (mask(0) & 0x1f)).toByte | |
val packetNumberOffset = 1 + dstConnId.size | |
protectedHeader(packetNumberOffset) = ((protectedHeader(packetNumberOffset) ^ mask(1))).toByte | |
ByteString(protectedHeader ++ encryptedPayload) | |
} | |
def nvarlen(i: Long): ByteString = { | |
require(i >= 0, s"varlen must be positive but was [$i]") | |
require(i < 63, s"too stupid to encode larger varlens like [$i]") | |
ByteString(i.toByte) | |
} | |
def ack(largestAcknowledged: Long, ackDelay: Long, ackRanges: Seq[Long]): ByteString = { | |
require(ackRanges.isEmpty) | |
nvarlen(0x2) ++ //ACK | |
nvarlen(largestAcknowledged) ++ | |
nvarlen(ackDelay) ++ | |
nvarlen(0) ++ nvarlen(0) | |
} | |
def createInitialClientPayload(): ByteString = { | |
def createClientHello(): ByteString = { | |
def ext(tpe: Int, payload: ByteString): ByteString = | |
uint16BE(tpe) ++ uint16BE(payload.size) ++ payload | |
val keyShareData = { | |
val ecPublic = pair.getPublic.asInstanceOf[ECPublicKey] | |
def leftPaddedTo(length: Int, bytes: ByteString): ByteString = | |
if (bytes.length == length + 1 && bytes(0) == 0) bytes.drop(1) | |
else { | |
require(bytes.length <= length) | |
ByteString(new Array[Byte](length - bytes.length)) ++ bytes | |
} | |
val x = ByteString(ecPublic.getW.getAffineX.toByteArray) | |
val y = ByteString(ecPublic.getW.getAffineY.toByteArray) | |
println(s"X size: ${x.size} Y size: ${y.size}") | |
val len = ecPublic.getParams.getCurve.getField.getFieldSize / 8 | |
assert("ec field size", 32, len) | |
val keyMaterial = "04".hexByteString ++ leftPaddedTo(len, x) ++ leftPaddedTo(len, y) | |
// only valid for ec params | |
val res = "00 17".hexByteString ++ uint16BE(keyMaterial.size) ++ keyMaterial | |
println(res.toArray.hexString) | |
res | |
} | |
val extensions: ByteString = Seq( | |
ext(0x00, "00 0d 00 00 0a 71 75 69 63 2e 72 6f 63 6b 73".hexByteString), // server name = quic.rocks | |
ext(0x0a, "00 02 00 17".hexByteString), // supported groups, only secp256r1 (because I found out how to generate keys for that) JDK 11 also knows about which would be preferred x25519 | |
ext(0x0d, "00 02 08 04".hexByteString), // support signature, only rsa_pss_rsae_sha256 (we do not verify signatures, so whatever) | |
ext(0x10, "00 06 05 68 33 2d 32 33".hexByteString), // alpn: "h3-23" | |
ext(0x2b, "02 03 04".hexByteString), // supported version tls 1.3 | |
ext(0x33, uint16BE(keyShareData.length) ++ keyShareData), // keyshare for above created pair | |
"FF A5 00 9B 00 99 00 01 00 04 80 00 75 30 00\n03 00 02 45 C0 00 04 00 04 80 F0 00 00 00 05 00\n04 80 00 40 00 00 06 00 04 80 00 40 00 00 07 00\n04 80 00 40 00 00 08 00 02 40 64 00 09 00 02 40\n67 47 51 00 53 00 00 00 00 04 00 00 00 55 41 49\n44 23 00 00 00 53 43 4C 53 27 00 00 00 43 4F 50\n54 27 00 00 00 49 52 54 54 2B 00 00 00 64 65 76\n20 43 68 72 6F 6D 65 2F 37 39 2E 30 2E 33 39 32\n38 2E 34 20 4C 69 6E 75 78 20 78 38 36 5F 36 34\n01 00 00 00 85 26 02 00 47 52 00 04 FF 00 00 17".hexByteString, | |
//ext(0xffa5, "00 08 00 01 00 04 80 00 75 30".hexByteString), // empty transport parameters | |
ext(0x15, ByteString(new Array[Byte](262))) // padding | |
).reduce(_ ++ _) | |
"03 03".hexByteString ++ // legacy version | |
ByteString(random.nextByteString(32)) ++ | |
"20".hexByteString ++ ByteString(random.nextByteString(32)) ++ // legacy session id | |
"00 02 13 01".hexByteString ++ // just offer TLS_AES_128_GCM_SHA256 (good to be able to use the same hkdf for initial and later encryption levels) | |
"01 00".hexByteString ++ // legacy compression method "null" | |
//"00 00".hexByteString ++ | |
uint16BE(extensions.length) ++ extensions | |
} | |
createCRYPTO { | |
val clientHello = handshakeMessage( | |
0x01, | |
createClientHello() | |
) | |
addToTranscript(clientHello) | |
clientHello | |
} | |
} | |
// Bytes that couldn't yet be read from a CRYPTO frame because data was still missing | |
var stashedCryptoBytes: Array[Byte] = Array.empty | |
var cryptoOffsets: Array[Long] = new Array[Long](4) | |
def assert[T](what: String, expected: T, actual: T): Unit = | |
require(expected == actual, s"expected [$what] as [$expected] but got [$actual]") | |
def analyzeInitialServerPacket(data: Array[Byte]): Unit = { | |
val reader = new ByteReader(data) | |
import reader._ | |
@tailrec def handleNextPacket(): Unit = | |
if (reader.hasRemaining) { | |
val maskedFlags = byte() | |
if ((maskedFlags & 0x80) == 0) // short header | |
handleShortHeaderPacket(maskedFlags) | |
else { | |
val packetType = (maskedFlags & 0xf0) | |
packetType match { | |
case 0xc0 => handleInitial(maskedFlags) | |
case 0xe0 => handleHandshake(maskedFlags) | |
case 0xf0 => handleRetry() | |
} | |
} | |
handleNextPacket() | |
} | |
handleNextPacket() | |
def handleInitial(maskedFlags: Int): Unit = { | |
//assert("packet type", 0xc0, packetType) | |
val version = uint32BE() | |
assert("version", 0xff000017L, version) | |
val dcidlen = byte() | |
val dcid = bytes(dcidlen) | |
println(s"received dest id ${dcid.toArray.hexString}") | |
val srcLen = byte() | |
val src = bytes(srcLen) | |
dstConnId = src | |
//val src = dstConnId | |
if (dstConnId != originalDstConnId) | |
println(s"Server changed its connId to $dstConnId") | |
val initialSecret = hkdf.extract(initialSalt, originalDstConnId.toArray) | |
val serverInitialSecret = hkdfExpandLabel(initialSecret, "server in", Array.empty, 32) | |
val serverKey = hkdfExpandLabel(serverInitialSecret, "quic key", Array.empty, 16) | |
val serverIv = hkdfExpandLabel(serverInitialSecret, "quic iv", Array.empty, 12) | |
val serverHp = hkdfExpandLabel(serverInitialSecret, "quic hp", Array.empty, 16) | |
val tokenLength = varlen() | |
assert("tokenLength", 0, tokenLength) | |
val payloadLength = varlen() | |
// assert("payloadLength", payloadLength, remaining) // otherwise more frames or packets in this packet | |
val afterLength = pos() | |
skip(4) | |
val sample = byteArray(16) | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES")) | |
val mask = hpCipher.doFinal(sample) | |
println(s"Mask: ${mask.hexString}") | |
val unmaskedFlagBits = (maskedFlags & 0x0f) ^ (mask(0) & 0x0f) | |
println(s"Flags: ${unmaskedFlagBits.toBinaryString}") | |
assert("reserved", 0, unmaskedFlagBits & 0x0c) | |
val pnLength = (unmaskedFlagBits & 0x03) + 1 | |
setPos(afterLength) | |
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long = | |
if (remaining == 0) res | |
else { | |
val next = (byte() ^ mask(atMask)).toByte | |
readPN(remaining - 1, atMask + 1, res << 8 | next) | |
} | |
val packetNumber = readPN(pnLength, 1, 0L) | |
println(s"PacketNumber: $packetNumber") | |
//assert("packetNumber", 1, packetNumber) | |
val plainHeader = data.take(pos()) | |
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x0f)).toByte | |
def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit = | |
if (remaining > 0) { | |
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte | |
unmaskHeader(remaining - 1, atMask + 1, pos + 1) | |
} | |
unmaskHeader(pnLength, 1, afterLength) | |
println(s"Plain header: ${plainHeader.hexString}") | |
val encryptedPayload = byteArray(payloadLength.toInt - pnLength) // FIXME, int? | |
val nonce = serverIv.clone() | |
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte | |
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte | |
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte | |
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte | |
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader) | |
val payload = payloadCipher.doFinal(encryptedPayload) | |
println(s"Payload: ${payload.hexString}") | |
send(createInitialPacket(ack(packetNumber, 0, Nil))) | |
readFrames(payload, 0) | |
} | |
def readFrames(payload: Array[Byte], encryptionLevel: Int): Unit = { | |
val payloadReader = new ByteReader(payload) | |
@tailrec def readNextFrame(): Unit = | |
if (payloadReader.hasRemaining) { | |
val frameType = payloadReader.varlen() | |
frameType match { | |
case 0x00 => // padding | |
case 0x01 => // PING | |
// nothing to do here | |
case 0x02 => // ACK | |
val largestAcknowledged = payloadReader.varlen() | |
val delay = payloadReader.varlen() | |
val count = payloadReader.varlen() | |
val first = payloadReader.varlen() | |
println(s"Got ACK largest: $largestAcknowledged delay: $delay count: $count first: $first") | |
case 0x06 => // CRYPTO | |
val offset = payloadReader.varlen() | |
val len = payloadReader.varlen() | |
val cryptoLoad = payloadReader.byteArray(len.toInt) | |
println(s"Got CRYPTO frame at offset $offset (encLevel: $encryptionLevel) of size $len: ${cryptoLoad.hexString}") | |
val cryptoOffset = cryptoOffsets(encryptionLevel) | |
if (offset != cryptoOffset) { | |
println(s"XXXXX Got retransmission on encLevel $encryptionLevel? offset: $offset != cryptoOffset $cryptoOffset, ignoring frame") | |
} else { | |
cryptoOffsets(encryptionLevel) = offset + len | |
val cryptoLoadReader = new ByteReader(stashedCryptoBytes ++ cryptoLoad) | |
@tailrec def readNextHandshakeMessage(): Unit = if (cryptoLoadReader.hasRemaining) { | |
val handshakeMessageTpe = cryptoLoadReader.byte() | |
println(s"Got handshake message of type [$handshakeMessageTpe]") | |
val length = cryptoLoadReader.uint24BE() | |
if (length <= cryptoLoadReader.remaining) { | |
val handshakeMessage = cryptoLoadReader.bytes(length) | |
addToTranscript(ByteString(handshakeMessageTpe.toByte) ++ SimpleQuicClientMain.uint24BE(length) ++ handshakeMessage) | |
handshakeMessageTpe match { | |
case 2 => // server_hello | |
println("Got ServerHello") | |
readServerHelloCRYPTO(handshakeMessage) | |
case 8 => // encrypted_extensions | |
println("got encrypted_extensions") | |
case 11 => // certificate | |
println("got certificate") | |
case 15 => // certificate_verify | |
println("got certificate_verify") | |
transcriptUntilCertificateVerify = currentTranscriptHash() | |
case 20 => // finished | |
println("got finished") | |
assert("finished size", 32, handshakeMessage.length) | |
val verifyData = handshakeMessage | |
println(s"finished verify_data: ${verifyData.toArray.hexString}") | |
val transcriptHash = currentTranscriptHash() | |
clientApplicationSecret = hkdfExpandLabel(masterSecret, "c ap traffic", transcriptHash, 32) | |
println(s"clientApplicationSecret: ${clientApplicationSecret.hexString}") | |
serverApplicationSecret = hkdfExpandLabel(masterSecret, "s ap traffic", transcriptHash, 32) | |
println(s"serverApplicationSecret: ${serverApplicationSecret.hexString}") | |
finishedKey = hkdfExpandLabel(serverHandshakeSecret, "finished", Array.empty, 32) | |
println(s"finishedKey: ${finishedKey.hexString}") | |
val mac = Mac.getInstance("HmacSHA256") | |
mac.init(new SecretKeySpec(finishedKey, "HmacSHA256")) | |
val check = mac.doFinal(transcriptUntilCertificateVerify) | |
assert("finish verification", ByteString(check), verifyData) | |
// FIXME: verify certificate | |
sendHandshakeFinished() | |
} | |
readNextHandshakeMessage() | |
} else { | |
println(s"Couldn't read next handshake message because some bytes were still missing. Stashing ${cryptoLoadReader.remaining} bytes (needed $length)") | |
cryptoLoadReader.setPos(cryptoLoadReader.pos() - 4) // rewind to get behind message type and length | |
stashedCryptoBytes = cryptoLoadReader.remainingByteArray | |
} | |
} | |
readNextHandshakeMessage() | |
} | |
case x if x >= 0x08 && x <= 0x0f => // STREAM | |
println("Got STREAM") | |
val offF = (x & 0x4) != 0 | |
val lenF = (x & 0x2) != 0 | |
val fin = (x & 0x1) != 0 | |
println(s"flags off: $offF len: $lenF fin: $fin") | |
val streamId = payloadReader.varlen() | |
println(s"streamId: $streamId server initiated: ${(streamId & 0x1) != 0} bidi: ${(streamId & 0x2) == 0}") | |
val off = if (offF) payloadReader.varlen() else 0L | |
val len = if (lenF) payloadReader.varlen() else -1 | |
println(s"off: $off len: $len") | |
val data = if (len == -1) payloadReader.remainingByteArray else payloadReader.byteArray(len.toInt) | |
println(s"data: ${data.hexString}") | |
handleStreamData(streamId, off, fin, data) | |
case 0x10 => // MAX_DATA | |
val maxData = payloadReader.varlen() | |
println(s"Got MAX_DATA $maxData") | |
case x @ (0x12 | 0x13) => // MAX_STREAMS | |
val bidi = (x & 1) != 0 | |
val maxStreams = payloadReader.varlen() | |
println(s"Got MAX_STREAMS $maxStreams bidi $bidi") | |
case 0x1c => // CONNECTION_CLOSE | |
val errorCode = payloadReader.varlen() | |
val frameType = payloadReader.varlen() | |
val reasonPhraseLen = payloadReader.varlen().toInt | |
val reasonPhrase = payloadReader.string(reasonPhraseLen) | |
println(s"Got error [$errorCode] '$reasonPhrase'") | |
case x => | |
println(s"Got frame of unknown type ${frameType.toHexString}") | |
??? | |
} | |
readNextFrame() | |
} | |
readNextFrame() | |
} | |
def readServerHelloCRYPTO(payload: ByteString): Unit = { | |
val reader = new ByteReader(payload.toArray) | |
import reader._ | |
val version = reader.uint16BE() | |
assert("tls legacy version", 0x0303, version) | |
reader.bytes(32) | |
val sessionLength = reader.byte() | |
val session = reader.bytes(sessionLength) | |
val cipherSuite = reader.uint16BE() | |
assert("cipher suite", 0x1301, cipherSuite) // TLS_AES_128_GCM_SHA256 | |
val compressionMethod = reader.byte() | |
assert("legacy compression method", 0, compressionMethod) | |
val extensionLength = reader.uint16BE() | |
@tailrec def readNextExtension(): Unit = | |
if (reader.hasRemaining) { | |
val tpe = reader.uint16BE() | |
val length = reader.uint16BE() | |
tpe match { | |
case 0x2b => // version | |
assert("version length", 2, length) | |
val version = reader.uint16BE() | |
assert("tls version", 0x0304, version) | |
case 0x33 => // key_share | |
val group = reader.uint16BE() | |
val keySize = reader.uint16BE() | |
val legacyForm = reader.byte() | |
val key = reader.byteArray(keySize - 1) | |
assert("DH group", 0x0017, group) | |
assert("ECDHE legacyForm", 4, legacyForm) | |
println(s"Got keymaterial of size ${key.size}: ${key.hexString}") | |
assert("ECDHE key size", 64, key.length) | |
val x = new BigInteger(key.take(32)) | |
val y = new BigInteger(key.drop(32)) | |
println(s"x: $x y: $y") | |
val agreement = KeyAgreement.getInstance("ECDH") | |
agreement.init(pair.getPrivate) | |
val params = pair.getPrivate.asInstanceOf[ECPrivateKey].getParams | |
val ecPublic = new ECPublicKeyImpl(new ECPoint(x, y), params) // FIXME: is there a way to do that with public API? | |
agreement.doPhase(ecPublic, true) | |
val secret = agreement.generateSecret() | |
println(s"Calculated secret of size ${secret.size}: ${secret.hexString}") | |
val digest = MessageDigest.getInstance("SHA-256") | |
val emptyHash = digest.digest(Array.empty[Byte]) | |
val early = hkdf.extract(new Array[Byte](32), new Array[Byte](32)) | |
println(s"Early: ${early.hexString}") | |
val preHandshake = hkdfExpandLabel(early, "derived", emptyHash, 32) | |
println(s"Pre handshake: ${preHandshake.hexString}") | |
handshakeSecret = hkdf.extract(preHandshake, secret) | |
println(s"Derived handshakeSecret: ${handshakeSecret.hexString}") | |
val transcriptHash = currentTranscriptHash() | |
serverHandshakeSecret = hkdfExpandLabel(handshakeSecret, "s hs traffic", transcriptHash, 32) | |
println(s"serverHandshakeTrafficSecret: ${serverHandshakeSecret.hexString}") | |
clientHandshakeSecret = hkdfExpandLabel(handshakeSecret, "c hs traffic", transcriptHash, 32) | |
println(s"clientHandshakeTrafficSecret: ${clientHandshakeSecret.hexString}") | |
val preMaster = hkdfExpandLabel(handshakeSecret, "derived", emptyHash, 32) | |
masterSecret = hkdf.extract(preMaster, new Array[Byte](32)) | |
println(s"masterSecret: ${masterSecret.hexString}") | |
case x => | |
println(s"Unknown extension [0x${x.toHexString}]") | |
reader.skip(length) | |
} | |
readNextExtension() | |
} | |
readNextExtension() | |
} | |
def handleHandshake(maskedFlags: Int): Unit = { | |
println("Got Handshake packet") | |
val startOfHeader = pos() - 1 | |
val quicVersion = uint32BE() | |
assert("quicVersion", 0xff000017L, quicVersion) | |
val dcidlen = byte() | |
val dcid = bytes(dcidlen) | |
assert("our dcid matches", srcConnId, dcid) | |
val srcLen = byte() | |
val src0 = bytes(srcLen) | |
val src = dstConnId | |
assert("their dcid matches", dstConnId, src0) | |
val payloadLength = varlen() | |
println(s"Payload size: $payloadLength") | |
val serverKey = hkdfExpandLabel(serverHandshakeSecret, "quic key", Array.empty, 16) | |
val serverIv = hkdfExpandLabel(serverHandshakeSecret, "quic iv", Array.empty, 12) | |
val serverHp = hkdfExpandLabel(serverHandshakeSecret, "quic hp", Array.empty, 16) | |
println(s"Handshake ServerKey: ${serverKey.hexString}") | |
println(s"Handshake ServerIv: ${serverIv.hexString}") | |
println(s"Handshake ServerHp: ${serverHp.hexString}") | |
val afterLength = pos() | |
skip(4) | |
val sample = byteArray(16) | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES")) | |
val mask = hpCipher.doFinal(sample) | |
println(s"Mask: ${mask.hexString}") | |
val unmaskedFlagBits = (maskedFlags & 0x0f) ^ (mask(0) & 0x0f) | |
println(s"Flags: ${unmaskedFlagBits.toBinaryString}") | |
assert("reserved", 0, unmaskedFlagBits & 0x0c) | |
val pnLength = (unmaskedFlagBits & 0x03) + 1 | |
setPos(afterLength) | |
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long = | |
if (remaining == 0) res | |
else { | |
val next = (byte() ^ mask(atMask)).toByte | |
readPN(remaining - 1, atMask + 1, res << 8 | next) | |
} | |
val packetNumber = readPN(pnLength, 1, 0L) | |
println(s"PacketNumber: $packetNumber") | |
//assert("packetNumber", 1, packetNumber) | |
val oldPos = pos() | |
setPos(startOfHeader) | |
val bytesToRead = oldPos - startOfHeader | |
println(s"bytesToRead: ${bytesToRead}") | |
val plainHeader = byteArray(oldPos - startOfHeader) | |
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x0f)).toByte | |
@tailrec def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit = | |
if (remaining > 0) { | |
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte | |
unmaskHeader(remaining - 1, atMask + 1, pos + 1) | |
} | |
unmaskHeader(pnLength, 1, afterLength - startOfHeader) | |
println(s"Plain header: ${plainHeader.hexString}") | |
val encryptedPayload = byteArray(payloadLength.toInt - pnLength) // FIXME, int? | |
val nonce = serverIv.clone() | |
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte | |
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte | |
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte | |
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte | |
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader) | |
val payload = payloadCipher.doFinal(encryptedPayload) | |
println(s"Payload: ${payload.hexString}") | |
send(createHandshakePacket(ack(packetNumber, 0, Nil))) | |
readFrames(payload, 1) | |
} | |
def handleShortHeaderPacket(maskedFlags: Int): Unit = { | |
val startOfHeader = pos() - 1 | |
val remaining = remainingByteArray | |
println(s"Got short header packet ${(maskedFlags.toByte +: remaining).hexString}") | |
setPos(startOfHeader + 1) | |
println(s"srcConnId: ${srcConnId.toArray.hexString}") | |
val dst = bytes(srcConnId.length) | |
println(s"dst: ${dst.toArray.hexString}") | |
assert("dcid", srcConnId, dst) | |
val serverKey = hkdfExpandLabel(serverApplicationSecret, "quic key", Array.empty, 16) | |
val serverIv = hkdfExpandLabel(serverApplicationSecret, "quic iv", Array.empty, 12) | |
val serverHp = hkdfExpandLabel(serverApplicationSecret, "quic hp", Array.empty, 16) | |
println(s"Application ServerKey: ${serverKey.hexString}") | |
println(s"Application ServerIv: ${serverIv.hexString}") | |
println(s"Application ServerHp: ${serverHp.hexString}") | |
val beforePacketNumber = pos() | |
skip(4) | |
val sample = byteArray(16) | |
hpCipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(serverHp, "AES")) | |
val mask = hpCipher.doFinal(sample) | |
println(s"Mask: ${mask.hexString}") | |
val unmaskedFlagBits = (maskedFlags & 0x1f) ^ (mask(0) & 0x1f) | |
println(s"Flags: ${unmaskedFlagBits.toBinaryString}") | |
val spin = (unmaskedFlagBits & 0x20) != 0 | |
println(s"Spin: $spin") | |
assert("reserved", 0, unmaskedFlagBits & 0x18) | |
val keyPhase = (unmaskedFlagBits & 0x04) != 0 | |
println(s"Keyphase: $keyPhase") | |
val pnLength = (unmaskedFlagBits & 0x03) + 1 | |
setPos(beforePacketNumber) | |
@tailrec def readPN(remaining: Int, atMask: Int, res: Long): Long = | |
if (remaining == 0) res | |
else { | |
val next = (byte() ^ mask(atMask)).toByte | |
readPN(remaining - 1, atMask + 1, res << 8 | next) | |
} | |
val packetNumber = readPN(pnLength, 1, 0L) | |
println(s"PacketNumber: $packetNumber") | |
//assert("packetNumber", 1, packetNumber) | |
val oldPos = pos() | |
setPos(startOfHeader) | |
val bytesToRead = oldPos - startOfHeader | |
println(s"bytesToRead: ${bytesToRead}") | |
val plainHeader = byteArray(oldPos - startOfHeader) | |
plainHeader(0) = (plainHeader(0) ^ (mask(0) & 0x1f)).toByte | |
@tailrec def unmaskHeader(remaining: Int, atMask: Int, pos: Int): Unit = | |
if (remaining > 0) { | |
plainHeader(pos) = (plainHeader(pos) ^ mask(atMask)).toByte | |
unmaskHeader(remaining - 1, atMask + 1, pos + 1) | |
} | |
unmaskHeader(pnLength, 1, beforePacketNumber - startOfHeader) | |
println(s"Plain header: ${plainHeader.hexString}") | |
val encryptedPayload = remainingByteArray | |
println(s"Encrypted payload: ${encryptedPayload.hexString}") | |
val nonce = serverIv.clone() | |
nonce(8) = (nonce(8) ^ ((packetNumber >> 24) & 0xff)).toByte | |
nonce(9) = (nonce(9) ^ ((packetNumber >> 16) & 0xff)).toByte | |
nonce(10) = (nonce(10) ^ ((packetNumber >> 8) & 0xff)).toByte | |
nonce(11) = (nonce(11) ^ ((packetNumber >> 0) & 0xff)).toByte | |
payloadCipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(serverKey, "AES"), new GCMParameterSpec(128, nonce)) | |
payloadCipher.updateAAD(plainHeader) | |
val payload = payloadCipher.doFinal(encryptedPayload) | |
println(s"Payload: ${payload.hexString}") | |
send(createShortPacket(ack(packetNumber, 0, Nil))) | |
readFrames(payload, 2) | |
} | |
def handleRetry(): Unit = { | |
println("Got Retry") | |
//assert("packet type", 0xf0, packetType) | |
val version = uint32BE() | |
assert("version", 0xff000017L, version) | |
val dcidlen = byte() | |
val dcid = bytes(dcidlen) | |
val srclen = byte() | |
val src = bytes(srclen) | |
val odcidlen = byte() | |
val ocdid = bytes(odcidlen) | |
assert("original cid", dstConnId, ocdid) | |
val token = remainingByteArray | |
println(s"Got token: ${token.hexString}, src: ${src.toArray.hexString}") | |
} | |
} | |
def sendHandshakeFinished(): Unit = { | |
val finishedKey = hkdfExpandLabel(clientHandshakeSecret, "finished", Array.empty, 32) | |
println(s"finishedKey: ${finishedKey.hexString}") | |
val mac = Mac.getInstance("HmacSHA256") | |
mac.init(new SecretKeySpec(finishedKey, "HmacSHA256")) | |
val verifyData = mac.doFinal(currentTranscriptHash()) | |
val finishedData = ByteString(verifyData) | |
val cryptoFrame = createCRYPTO(handshakeMessage(0x14 /* Finished */ , finishedData)) | |
println(s"Sending Finished CRYPTO [${cryptoFrame.toArray.hexString}]") | |
send(createHandshakePacket(cryptoFrame)) | |
sendSettings() | |
} | |
def sendSettings(): Unit = { | |
val settingsStream = "0A 02 07 00 04 04 01 00 07 00".hexByteString // same as received from server | |
val authority = s"${target.getHostName}:$targetPort" | |
val path = "/" | |
val userAgent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:69.0) Gecko/20100101 Firefox/69.0" | |
val headerBlock: ByteString = | |
"00 00 d1 d7 50".hexByteString ++ ByteString(authority.length.toByte) ++ ByteString(authority) ++ | |
"51".hexByteString ++ ByteString(path.length.toByte) ++ ByteString(path) //++ | |
// "5f 50".hexByteString ++ ByteString(userAgent.length.toByte) ++ ByteString(userAgent) | |
val headersFrame: ByteString = "01".hexByteString ++ nvarlen(headerBlock.size) ++ headerBlock | |
val request = "0B 08".hexByteString ++ nvarlen(headersFrame.size) ++ headersFrame | |
send(createShortPacket(settingsStream ++ request)) | |
} | |
var stashedDataForStream8: Array[Byte] = Array.empty | |
def handleStreamData(streamId: Long, offset: Long, fin: Boolean, data: Array[Byte]): Unit = streamId match { | |
case 0x8 => | |
println(s"Got response for stream 8 !!! offset $offset fin $fin") | |
val streamReader = new ByteReader(stashedDataForStream8 ++ data) | |
stashedDataForStream8 = Array.empty | |
@tailrec def handleHttp3Frame(): Unit = if (streamReader.hasRemaining) { | |
val startOfFrame = streamReader.pos() | |
val tpe = streamReader.varlen() | |
val len = streamReader.varlen() | |
if (len > streamReader.remaining) { | |
println(s"frame not complete need to wait for more data. Needed $len but only got ${streamReader.remaining}") | |
streamReader.setPos(startOfFrame) | |
stashedDataForStream8 = streamReader.remainingByteArray | |
println(s"stashed ${stashedDataForStream8.hexString}") | |
} else { | |
val payload = streamReader.bytes(len.toInt) | |
println(s"Got HTTP3 frame of type $tpe len $len (remaining ${streamReader.remaining})") | |
try tpe match { | |
case 0x0 => // DATA | |
println(s"Got response DATA: [${payload.utf8String}]") | |
case 0x1 => // HEADERS | |
println(s"Got response HEADERS ${payload.toArray.hexString}") | |
val payloadReader = new ByteReader(payload.toArray) | |
val reqInsertCount = payloadReader.byte() // FIXME 8-bit prefixed | |
val deltaBaseAndFlag = payloadReader.byte() | |
val sign = (deltaBaseAndFlag & 0x80) != 0 | |
val deltaBase = deltaBaseAndFlag & 0x7f | |
println(s"Prefix: reqInsertCount $reqInsertCount sign $sign deltaBase $deltaBase") | |
QPackTable.readField(payloadReader) | |
} | |
catch { | |
case NonFatal(ex) => | |
ex.printStackTrace() | |
} | |
handleHttp3Frame() | |
} | |
} | |
handleHttp3Frame() | |
case x => | |
println(s"Got data for stream $streamId at offset $offset ${data.hexString}") | |
} | |
val initialPacket = createInitialPacket(createInitialClientPayload()) | |
println(s"Initial packet: ${initialPacket.toArray.hexString}") | |
send(initialPacket) | |
val receiveBuffer = new Array[Byte](2000) | |
val receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.size) | |
def receiveOne(): Unit = { | |
socket.receive(receivePacket) | |
val receivedData = receivePacket.getData.take(receivePacket.getLength) | |
println(s"Packet received! $receivePacket ${receivedData.hexString}") | |
analyzeInitialServerPacket(receivedData) | |
} | |
while (true) receiveOne() | |
} | |
class ByteReader(data: Array[Byte]) { | |
private[this] var offset = 0 | |
def remaining: Int = data.length - offset | |
def hasRemaining: Boolean = offset < data.length | |
def byte(): Int = { | |
val res = data(offset) & 0xff | |
offset += 1 | |
res | |
} | |
def uint16BE(): Int = byte() << 8 | byte() | |
def uint24BE(): Int = (uint16BE() << 8) | byte() | |
def uint32BE(): Long = (uint24BE().toLong << 8L) | byte().toLong | |
def byteArray(len: Int): Array[Byte] = { | |
require(len <= remaining, s"Wanted to read $len further bytes but only have $remaining") | |
val res = data.slice(offset, offset + len) | |
offset += len | |
res | |
} | |
def bytes(len: Int): ByteString = ByteString(byteArray(len)) | |
def remainingByteArray: Array[Byte] = { | |
val res = data.drop(offset) | |
offset = data.length | |
res | |
} | |
def varlen(): Long = { | |
val first = byte() | |
val flags = (first & 0xc0) >> 6 | |
val firstData = first & 0x3f | |
flags match { | |
case 0 => first | |
case 1 => firstData << 8 | byte() | |
case 2 => firstData << 24 | uint24BE() | |
case 3 => firstData << 56 | uint24BE() << 32 | uint24BE() << 8 | byte() | |
} | |
} | |
def string(len: Int): String = bytes(len).utf8String | |
def skip(len: Int): Unit = offset += len | |
def pos(): Int = offset | |
def setPos(newPos: Int): Unit = offset = newPos | |
def prefixed(prefixLength: Int, prefix: Int): Long = | |
if (Integer.numberOfTrailingZeros(~prefix) == prefixLength) { | |
@tailrec def readOne(curV: Long, shift: Int): Long = { | |
val b = byte() | |
val newBits = b & 0x7f | |
val value = curV | (newBits << shift) | |
if ((b & 0x80) == 0) value | |
else readOne(value, shift + 7) | |
} | |
readOne(0, 0) + prefix | |
} else prefix | |
} | |
object QPackTable { | |
@tailrec def readField(payloadReader: ByteReader): Unit = if (payloadReader.hasRemaining) { | |
val b = payloadReader.byte() | |
if ((b & 0x80) != 0) { // indexed header field | |
val static = (b & 0x40) != 0 | |
val indexP = b & 0x3f | |
val index = payloadReader.prefixed(6, indexP) | |
println(s"Indexed Header Field static $static idx $index") | |
println(s"Decoded: ${QPackTable.entries(index.toInt)}") | |
} else if ((b & 0x40) != 0) { // literal header field with name reference | |
val nbit = (b & 0x20) != 0 | |
val nameS = (b & 0x10) != 0 | |
val nameIdxP = b & 0x0f | |
val nameIdx = payloadReader.prefixed(4, nameIdxP) | |
val valueB = payloadReader.byte() | |
val huffman = (valueB & 0x80) != 0 | |
val valueLP = valueB & 0x7f | |
val valueL = payloadReader.prefixed(7, valueLP) | |
val value = payloadReader.bytes(valueL.toInt) | |
println(s"literal header field with name reference nbit $nbit nameS $nameS nameIdx $nameIdx huffman $huffman valueL $valueL value ${value.toArray.hexString}") | |
println(s"Decoded: ${QPackTable.entries(nameIdx.toInt)._1} ${decode(huffman, value).utf8String}") | |
} else if ((b & 0x20) != 0) { // literal header field without name reference | |
val nbit = (b & 0x10) != 0 | |
val nameH = (b & 0x08) != 0 | |
val nameLP = b & 0x07 | |
val nameL = payloadReader.prefixed(3, nameLP) | |
val name = payloadReader.bytes(nameL.toInt) | |
val valueB = payloadReader.byte() | |
val valueH = (valueB & 0x80) != 0 | |
val valueLP = (valueB & 0x7f) | |
val valueL = payloadReader.prefixed(7, valueLP) | |
val value = payloadReader.bytes(valueL.toInt) | |
println(s"literal header field without name reference nbit $nbit nameH $nameH nameL $nameL name ${name.toArray.hexString} valueH $valueH valueL $valueL value ${value.toArray.hexString}") | |
println(s"Decoded: ${decode(nameH, name).utf8String} ${decode(valueH, value).utf8String}") | |
} else if ((b & 0x10) != 0) { // indexed Header Field With Post-Base Index | |
??? | |
} else { // Literal Header Field With Post-Base Name Reference | |
??? | |
} | |
readField(payloadReader) | |
} | |
def decode(huffman: Boolean, bytes: ByteString): ByteString = | |
if (huffman) HPackHuffman.decode(bytes) | |
else bytes | |
val entries: Seq[(String, Option[String])] = { | |
table.split("\n").filter(x => x.length > 43 && x(10) == '|' && (!x(12).isWhitespace || !x(43).isWhitespace)) | |
.drop(1) | |
.sliding(2) | |
.flatMap { | |
case Array(l1, l2) => | |
if (l1(5).isDigit && l2(5).isDigit) { | |
val Array(idx, name, value) = l1.split('|').drop(1).map(_.trim) | |
(idx.toInt, name, Some(value).filter(_.nonEmpty)) :: Nil | |
} else if (l1(5).isDigit) { | |
val Array(idx, name1, value1) = l1.split('|').drop(1).map(_.trim) | |
val Array(name2, value2) = l2.split('|').drop(2).map(_.trim) | |
(idx.toInt, name1 + name2, Some(value1 + value2).filter(_.nonEmpty)) :: Nil | |
} else Nil // ignore | |
} | |
.zipWithIndex | |
.map { | |
case ((idx, name, value), i) => | |
require(idx == i, s"Idx $idx and i $i didn't match") | |
(name, value) | |
} | |
.toSeq | |
} | |
private lazy val table = | |
"""Appendix A. Static Table | |
| | |
| +------+-----------------------------+------------------------------+ | |
| | Inde | Name | Value | | |
| | x | | | | |
| +------+-----------------------------+------------------------------+ | |
| | 0 | :authority | | | |
| | | | | | |
| | 1 | :path | / | | |
| | | | | | |
| | 2 | age | 0 | | |
| | | | | | |
| | 3 | content-disposition | | | |
| | | | | | |
| | 4 | content-length | 0 | | |
| | | | | | |
| | 5 | cookie | | | |
| | | | | | |
| | 6 | date | | | |
| | | | | | |
| | 7 | etag | | | |
| | | | | | |
| | 8 | if-modified-since | | | |
| | | | | | |
| | 9 | if-none-match | | | |
| | | | | | |
| | 10 | last-modified | | | |
| | | | | | |
| | 11 | link | | | |
| | |
| | |
| | |
|Krasic, et al. Expires March 15, 2020 [Page 28] | |
| | |
| | |
|Internet-Draft QPACK September 2019 | |
| | |
| | |
| | | | | | |
| | 12 | location | | | |
| | | | | | |
| | 13 | referer | | | |
| | | | | | |
| | 14 | set-cookie | | | |
| | | | | | |
| | 15 | :method | CONNECT | | |
| | | | | | |
| | 16 | :method | DELETE | | |
| | | | | | |
| | 17 | :method | GET | | |
| | | | | | |
| | 18 | :method | HEAD | | |
| | | | | | |
| | 19 | :method | OPTIONS | | |
| | | | | | |
| | 20 | :method | POST | | |
| | | | | | |
| | 21 | :method | PUT | | |
| | | | | | |
| | 22 | :scheme | http | | |
| | | | | | |
| | 23 | :scheme | https | | |
| | | | | | |
| | 24 | :status | 103 | | |
| | | | | | |
| | 25 | :status | 200 | | |
| | | | | | |
| | 26 | :status | 304 | | |
| | | | | | |
| | 27 | :status | 404 | | |
| | | | | | |
| | 28 | :status | 503 | | |
| | | | | | |
| | 29 | accept | */* | | |
| | | | | | |
| | 30 | accept | application/dns-message | | |
| | | | | | |
| | 31 | accept-encoding | gzip, deflate, br | | |
| | | | | | |
| | 32 | accept-ranges | bytes | | |
| | | | | | |
| | 33 | access-control-allow- | cache-control | | |
| | | headers | | | |
| | | | | | |
| | 34 | access-control-allow- | content-type | | |
| | | headers | | | |
| | |
| | |
| | |
|Krasic, et al. Expires March 15, 2020 [Page 29] | |
| | |
| | |
|Internet-Draft QPACK September 2019 | |
| | |
| | |
| | | | | | |
| | 35 | access-control-allow-origin | * | | |
| | | | | | |
| | 36 | cache-control | max-age=0 | | |
| | | | | | |
| | 37 | cache-control | max-age=2592000 | | |
| | | | | | |
| | 38 | cache-control | max-age=604800 | | |
| | | | | | |
| | 39 | cache-control | no-cache | | |
| | | | | | |
| | 40 | cache-control | no-store | | |
| | | | | | |
| | 41 | cache-control | public, max-age=31536000 | | |
| | | | | | |
| | 42 | content-encoding | br | | |
| | | | | | |
| | 43 | content-encoding | gzip | | |
| | | | | | |
| | 44 | content-type | application/dns-message | | |
| | | | | | |
| | 45 | content-type | application/javascript | | |
| | | | | | |
| | 46 | content-type | application/json | | |
| | | | | | |
| | 47 | content-type | application/x-www-form- | | |
| | | | urlencoded | | |
| | | | | | |
| | 48 | content-type | image/gif | | |
| | | | | | |
| | 49 | content-type | image/jpeg | | |
| | | | | | |
| | 50 | content-type | image/png | | |
| | | | | | |
| | 51 | content-type | text/css | | |
| | | | | | |
| | 52 | content-type | text/html; charset=utf-8 | | |
| | | | | | |
| | 53 | content-type | text/plain | | |
| | | | | | |
| | 54 | content-type | text/plain;charset=utf-8 | | |
| | | | | | |
| | 55 | range | bytes=0- | | |
| | | | | | |
| | 56 | strict-transport-security | max-age=31536000 | | |
| | | | | | |
| | 57 | strict-transport-security | max-age=31536000; | | |
| | | | includesubdomains | | |
| | |
| | |
| | |
|Krasic, et al. Expires March 15, 2020 [Page 30] | |
| | |
| | |
|Internet-Draft QPACK September 2019 | |
| | |
| | |
| | | | | | |
| | 58 | strict-transport-security | max-age=31536000; | | |
| | | | includesubdomains; preload | | |
| | | | | | |
| | 59 | vary | accept-encoding | | |
| | | | | | |
| | 60 | vary | origin | | |
| | | | | | |
| | 61 | x-content-type-options | nosniff | | |
| | | | | | |
| | 62 | x-xss-protection | 1; mode=block | | |
| | | | | | |
| | 63 | :status | 100 | | |
| | | | | | |
| | 64 | :status | 204 | | |
| | | | | | |
| | 65 | :status | 206 | | |
| | | | | | |
| | 66 | :status | 302 | | |
| | | | | | |
| | 67 | :status | 400 | | |
| | | | | | |
| | 68 | :status | 403 | | |
| | | | | | |
| | 69 | :status | 421 | | |
| | | | | | |
| | 70 | :status | 425 | | |
| | | | | | |
| | 71 | :status | 500 | | |
| | | | | | |
| | 72 | accept-language | | | |
| | | | | | |
| | 73 | access-control-allow- | FALSE | | |
| | | credentials | | | |
| | | | | | |
| | 74 | access-control-allow- | TRUE | | |
| | | credentials | | | |
| | | | | | |
| | 75 | access-control-allow- | * | | |
| | | headers | | | |
| | | | | | |
| | 76 | access-control-allow- | get | | |
| | | methods | | | |
| | | | | | |
| | 77 | access-control-allow- | get, post, options | | |
| | | methods | | | |
| | | | | | |
| | 78 | access-control-allow- | options | | |
| | |
| | |
| | |
|Krasic, et al. Expires March 15, 2020 [Page 31] | |
| | |
| | |
|Internet-Draft QPACK September 2019 | |
| | |
| | |
| | | methods | | | |
| | | | | | |
| | 79 | access-control-expose- | content-length | | |
| | | headers | | | |
| | | | | | |
| | 80 | access-control-request- | content-type | | |
| | | headers | | | |
| | | | | | |
| | 81 | access-control-request- | get | | |
| | | method | | | |
| | | | | | |
| | 82 | access-control-request- | post | | |
| | | method | | | |
| | | | | | |
| | 83 | alt-svc | clear | | |
| | | | | | |
| | 84 | authorization | | | |
| | | | | | |
| | 85 | content-security-policy | script-src 'none'; object- | | |
| | | | src 'none'; base-uri 'none' | | |
| | | | | | |
| | 86 | early-data | 1 | | |
| | | | | | |
| | 87 | expect-ct | | | |
| | | | | | |
| | 88 | forwarded | | | |
| | | | | | |
| | 89 | if-range | | | |
| | | | | | |
| | 90 | origin | | | |
| | | | | | |
| | 91 | purpose | prefetch | | |
| | | | | | |
| | 92 | server | | | |
| | | | | | |
| | 93 | timing-allow-origin | * | | |
| | | | | | |
| | 94 | upgrade-insecure-requests | 1 | | |
| | | | | | |
| | 95 | user-agent | | | |
| | | | | | |
| | 96 | x-forwarded-for | | | |
| | | | | | |
| | 97 | x-frame-options | deny | | |
| | | | | | |
| | 98 | x-frame-options | sameorigin | | |
| +------+-----------------------------+------------------------------+""".stripMargin | |
} | |
object HPackHuffman { | |
def decode(bytes: ByteString): ByteString = { | |
val bb = new ByteStringBuilder | |
@tailrec def decodeNext(remaining: String): Unit = if (remaining.nonEmpty) { | |
val i = codes.indexWhere(remaining.startsWith) | |
i match { | |
case -1 => | |
// done | |
require(remaining.size < 8, s"Couldn't find prefix when size was ${remaining.size}: [$remaining]") | |
case 256 => // done | |
case x => | |
bb += x.toByte | |
decodeNext(remaining.drop(codes(i).length)) | |
} | |
} | |
// string-based huffman decoding is really the best | |
val allBits = bytes.map(b => ((b & 0xff) | 0x100).toBinaryString.takeRight(8)).mkString | |
decodeNext(allBits) | |
bb.result() | |
} | |
val codes: Seq[String] = | |
table.split("\n") | |
.filter(x => x.length > 14 && x(14) == '|') | |
.map(_.slice(14, 49).filter(_ != '|').trim) | |
private lazy val table = | |
""" code | |
| code as bits as hex len | |
| sym aligned to MSB aligned in | |
| to LSB bits | |
| ( 0) |11111111|11000 1ff8 [13] | |
| ( 1) |11111111|11111111|1011000 7fffd8 [23] | |
| ( 2) |11111111|11111111|11111110|0010 fffffe2 [28] | |
| ( 3) |11111111|11111111|11111110|0011 fffffe3 [28] | |
| ( 4) |11111111|11111111|11111110|0100 fffffe4 [28] | |
| ( 5) |11111111|11111111|11111110|0101 fffffe5 [28] | |
| ( 6) |11111111|11111111|11111110|0110 fffffe6 [28] | |
| ( 7) |11111111|11111111|11111110|0111 fffffe7 [28] | |
| ( 8) |11111111|11111111|11111110|1000 fffffe8 [28] | |
| ( 9) |11111111|11111111|11101010 ffffea [24] | |
| ( 10) |11111111|11111111|11111111|111100 3ffffffc [30] | |
| ( 11) |11111111|11111111|11111110|1001 fffffe9 [28] | |
| ( 12) |11111111|11111111|11111110|1010 fffffea [28] | |
| ( 13) |11111111|11111111|11111111|111101 3ffffffd [30] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 27] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| ( 14) |11111111|11111111|11111110|1011 fffffeb [28] | |
| ( 15) |11111111|11111111|11111110|1100 fffffec [28] | |
| ( 16) |11111111|11111111|11111110|1101 fffffed [28] | |
| ( 17) |11111111|11111111|11111110|1110 fffffee [28] | |
| ( 18) |11111111|11111111|11111110|1111 fffffef [28] | |
| ( 19) |11111111|11111111|11111111|0000 ffffff0 [28] | |
| ( 20) |11111111|11111111|11111111|0001 ffffff1 [28] | |
| ( 21) |11111111|11111111|11111111|0010 ffffff2 [28] | |
| ( 22) |11111111|11111111|11111111|111110 3ffffffe [30] | |
| ( 23) |11111111|11111111|11111111|0011 ffffff3 [28] | |
| ( 24) |11111111|11111111|11111111|0100 ffffff4 [28] | |
| ( 25) |11111111|11111111|11111111|0101 ffffff5 [28] | |
| ( 26) |11111111|11111111|11111111|0110 ffffff6 [28] | |
| ( 27) |11111111|11111111|11111111|0111 ffffff7 [28] | |
| ( 28) |11111111|11111111|11111111|1000 ffffff8 [28] | |
| ( 29) |11111111|11111111|11111111|1001 ffffff9 [28] | |
| ( 30) |11111111|11111111|11111111|1010 ffffffa [28] | |
| ( 31) |11111111|11111111|11111111|1011 ffffffb [28] | |
| ' ' ( 32) |010100 14 [ 6] | |
| '!' ( 33) |11111110|00 3f8 [10] | |
| '"' ( 34) |11111110|01 3f9 [10] | |
| '#' ( 35) |11111111|1010 ffa [12] | |
| '$' ( 36) |11111111|11001 1ff9 [13] | |
| '%' ( 37) |010101 15 [ 6] | |
| '&' ( 38) |11111000 f8 [ 8] | |
| ''' ( 39) |11111111|010 7fa [11] | |
| '(' ( 40) |11111110|10 3fa [10] | |
| ')' ( 41) |11111110|11 3fb [10] | |
| '*' ( 42) |11111001 f9 [ 8] | |
| '+' ( 43) |11111111|011 7fb [11] | |
| ',' ( 44) |11111010 fa [ 8] | |
| '-' ( 45) |010110 16 [ 6] | |
| '.' ( 46) |010111 17 [ 6] | |
| '/' ( 47) |011000 18 [ 6] | |
| '0' ( 48) |00000 0 [ 5] | |
| '1' ( 49) |00001 1 [ 5] | |
| '2' ( 50) |00010 2 [ 5] | |
| '3' ( 51) |011001 19 [ 6] | |
| '4' ( 52) |011010 1a [ 6] | |
| '5' ( 53) |011011 1b [ 6] | |
| '6' ( 54) |011100 1c [ 6] | |
| '7' ( 55) |011101 1d [ 6] | |
| '8' ( 56) |011110 1e [ 6] | |
| '9' ( 57) |011111 1f [ 6] | |
| ':' ( 58) |1011100 5c [ 7] | |
| ';' ( 59) |11111011 fb [ 8] | |
| '<' ( 60) |11111111|1111100 7ffc [15] | |
| '=' ( 61) |100000 20 [ 6] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 28] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| '>' ( 62) |11111111|1011 ffb [12] | |
| '?' ( 63) |11111111|00 3fc [10] | |
| '@' ( 64) |11111111|11010 1ffa [13] | |
| 'A' ( 65) |100001 21 [ 6] | |
| 'B' ( 66) |1011101 5d [ 7] | |
| 'C' ( 67) |1011110 5e [ 7] | |
| 'D' ( 68) |1011111 5f [ 7] | |
| 'E' ( 69) |1100000 60 [ 7] | |
| 'F' ( 70) |1100001 61 [ 7] | |
| 'G' ( 71) |1100010 62 [ 7] | |
| 'H' ( 72) |1100011 63 [ 7] | |
| 'I' ( 73) |1100100 64 [ 7] | |
| 'J' ( 74) |1100101 65 [ 7] | |
| 'K' ( 75) |1100110 66 [ 7] | |
| 'L' ( 76) |1100111 67 [ 7] | |
| 'M' ( 77) |1101000 68 [ 7] | |
| 'N' ( 78) |1101001 69 [ 7] | |
| 'O' ( 79) |1101010 6a [ 7] | |
| 'P' ( 80) |1101011 6b [ 7] | |
| 'Q' ( 81) |1101100 6c [ 7] | |
| 'R' ( 82) |1101101 6d [ 7] | |
| 'S' ( 83) |1101110 6e [ 7] | |
| 'T' ( 84) |1101111 6f [ 7] | |
| 'U' ( 85) |1110000 70 [ 7] | |
| 'V' ( 86) |1110001 71 [ 7] | |
| 'W' ( 87) |1110010 72 [ 7] | |
| 'X' ( 88) |11111100 fc [ 8] | |
| 'Y' ( 89) |1110011 73 [ 7] | |
| 'Z' ( 90) |11111101 fd [ 8] | |
| '[' ( 91) |11111111|11011 1ffb [13] | |
| '\' ( 92) |11111111|11111110|000 7fff0 [19] | |
| ']' ( 93) |11111111|11100 1ffc [13] | |
| '^' ( 94) |11111111|111100 3ffc [14] | |
| '_' ( 95) |100010 22 [ 6] | |
| '`' ( 96) |11111111|1111101 7ffd [15] | |
| 'a' ( 97) |00011 3 [ 5] | |
| 'b' ( 98) |100011 23 [ 6] | |
| 'c' ( 99) |00100 4 [ 5] | |
| 'd' (100) |100100 24 [ 6] | |
| 'e' (101) |00101 5 [ 5] | |
| 'f' (102) |100101 25 [ 6] | |
| 'g' (103) |100110 26 [ 6] | |
| 'h' (104) |100111 27 [ 6] | |
| 'i' (105) |00110 6 [ 5] | |
| 'j' (106) |1110100 74 [ 7] | |
| 'k' (107) |1110101 75 [ 7] | |
| 'l' (108) |101000 28 [ 6] | |
| 'm' (109) |101001 29 [ 6] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 29] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| 'n' (110) |101010 2a [ 6] | |
| 'o' (111) |00111 7 [ 5] | |
| 'p' (112) |101011 2b [ 6] | |
| 'q' (113) |1110110 76 [ 7] | |
| 'r' (114) |101100 2c [ 6] | |
| 's' (115) |01000 8 [ 5] | |
| 't' (116) |01001 9 [ 5] | |
| 'u' (117) |101101 2d [ 6] | |
| 'v' (118) |1110111 77 [ 7] | |
| 'w' (119) |1111000 78 [ 7] | |
| 'x' (120) |1111001 79 [ 7] | |
| 'y' (121) |1111010 7a [ 7] | |
| 'z' (122) |1111011 7b [ 7] | |
| '{' (123) |11111111|1111110 7ffe [15] | |
| '|' (124) |11111111|100 7fc [11] | |
| '}' (125) |11111111|111101 3ffd [14] | |
| '~' (126) |11111111|11101 1ffd [13] | |
| (127) |11111111|11111111|11111111|1100 ffffffc [28] | |
| (128) |11111111|11111110|0110 fffe6 [20] | |
| (129) |11111111|11111111|010010 3fffd2 [22] | |
| (130) |11111111|11111110|0111 fffe7 [20] | |
| (131) |11111111|11111110|1000 fffe8 [20] | |
| (132) |11111111|11111111|010011 3fffd3 [22] | |
| (133) |11111111|11111111|010100 3fffd4 [22] | |
| (134) |11111111|11111111|010101 3fffd5 [22] | |
| (135) |11111111|11111111|1011001 7fffd9 [23] | |
| (136) |11111111|11111111|010110 3fffd6 [22] | |
| (137) |11111111|11111111|1011010 7fffda [23] | |
| (138) |11111111|11111111|1011011 7fffdb [23] | |
| (139) |11111111|11111111|1011100 7fffdc [23] | |
| (140) |11111111|11111111|1011101 7fffdd [23] | |
| (141) |11111111|11111111|1011110 7fffde [23] | |
| (142) |11111111|11111111|11101011 ffffeb [24] | |
| (143) |11111111|11111111|1011111 7fffdf [23] | |
| (144) |11111111|11111111|11101100 ffffec [24] | |
| (145) |11111111|11111111|11101101 ffffed [24] | |
| (146) |11111111|11111111|010111 3fffd7 [22] | |
| (147) |11111111|11111111|1100000 7fffe0 [23] | |
| (148) |11111111|11111111|11101110 ffffee [24] | |
| (149) |11111111|11111111|1100001 7fffe1 [23] | |
| (150) |11111111|11111111|1100010 7fffe2 [23] | |
| (151) |11111111|11111111|1100011 7fffe3 [23] | |
| (152) |11111111|11111111|1100100 7fffe4 [23] | |
| (153) |11111111|11111110|11100 1fffdc [21] | |
| (154) |11111111|11111111|011000 3fffd8 [22] | |
| (155) |11111111|11111111|1100101 7fffe5 [23] | |
| (156) |11111111|11111111|011001 3fffd9 [22] | |
| (157) |11111111|11111111|1100110 7fffe6 [23] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 30] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| (158) |11111111|11111111|1100111 7fffe7 [23] | |
| (159) |11111111|11111111|11101111 ffffef [24] | |
| (160) |11111111|11111111|011010 3fffda [22] | |
| (161) |11111111|11111110|11101 1fffdd [21] | |
| (162) |11111111|11111110|1001 fffe9 [20] | |
| (163) |11111111|11111111|011011 3fffdb [22] | |
| (164) |11111111|11111111|011100 3fffdc [22] | |
| (165) |11111111|11111111|1101000 7fffe8 [23] | |
| (166) |11111111|11111111|1101001 7fffe9 [23] | |
| (167) |11111111|11111110|11110 1fffde [21] | |
| (168) |11111111|11111111|1101010 7fffea [23] | |
| (169) |11111111|11111111|011101 3fffdd [22] | |
| (170) |11111111|11111111|011110 3fffde [22] | |
| (171) |11111111|11111111|11110000 fffff0 [24] | |
| (172) |11111111|11111110|11111 1fffdf [21] | |
| (173) |11111111|11111111|011111 3fffdf [22] | |
| (174) |11111111|11111111|1101011 7fffeb [23] | |
| (175) |11111111|11111111|1101100 7fffec [23] | |
| (176) |11111111|11111111|00000 1fffe0 [21] | |
| (177) |11111111|11111111|00001 1fffe1 [21] | |
| (178) |11111111|11111111|100000 3fffe0 [22] | |
| (179) |11111111|11111111|00010 1fffe2 [21] | |
| (180) |11111111|11111111|1101101 7fffed [23] | |
| (181) |11111111|11111111|100001 3fffe1 [22] | |
| (182) |11111111|11111111|1101110 7fffee [23] | |
| (183) |11111111|11111111|1101111 7fffef [23] | |
| (184) |11111111|11111110|1010 fffea [20] | |
| (185) |11111111|11111111|100010 3fffe2 [22] | |
| (186) |11111111|11111111|100011 3fffe3 [22] | |
| (187) |11111111|11111111|100100 3fffe4 [22] | |
| (188) |11111111|11111111|1110000 7ffff0 [23] | |
| (189) |11111111|11111111|100101 3fffe5 [22] | |
| (190) |11111111|11111111|100110 3fffe6 [22] | |
| (191) |11111111|11111111|1110001 7ffff1 [23] | |
| (192) |11111111|11111111|11111000|00 3ffffe0 [26] | |
| (193) |11111111|11111111|11111000|01 3ffffe1 [26] | |
| (194) |11111111|11111110|1011 fffeb [20] | |
| (195) |11111111|11111110|001 7fff1 [19] | |
| (196) |11111111|11111111|100111 3fffe7 [22] | |
| (197) |11111111|11111111|1110010 7ffff2 [23] | |
| (198) |11111111|11111111|101000 3fffe8 [22] | |
| (199) |11111111|11111111|11110110|0 1ffffec [25] | |
| (200) |11111111|11111111|11111000|10 3ffffe2 [26] | |
| (201) |11111111|11111111|11111000|11 3ffffe3 [26] | |
| (202) |11111111|11111111|11111001|00 3ffffe4 [26] | |
| (203) |11111111|11111111|11111011|110 7ffffde [27] | |
| (204) |11111111|11111111|11111011|111 7ffffdf [27] | |
| (205) |11111111|11111111|11111001|01 3ffffe5 [26] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 31] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| (206) |11111111|11111111|11110001 fffff1 [24] | |
| (207) |11111111|11111111|11110110|1 1ffffed [25] | |
| (208) |11111111|11111110|010 7fff2 [19] | |
| (209) |11111111|11111111|00011 1fffe3 [21] | |
| (210) |11111111|11111111|11111001|10 3ffffe6 [26] | |
| (211) |11111111|11111111|11111100|000 7ffffe0 [27] | |
| (212) |11111111|11111111|11111100|001 7ffffe1 [27] | |
| (213) |11111111|11111111|11111001|11 3ffffe7 [26] | |
| (214) |11111111|11111111|11111100|010 7ffffe2 [27] | |
| (215) |11111111|11111111|11110010 fffff2 [24] | |
| (216) |11111111|11111111|00100 1fffe4 [21] | |
| (217) |11111111|11111111|00101 1fffe5 [21] | |
| (218) |11111111|11111111|11111010|00 3ffffe8 [26] | |
| (219) |11111111|11111111|11111010|01 3ffffe9 [26] | |
| (220) |11111111|11111111|11111111|1101 ffffffd [28] | |
| (221) |11111111|11111111|11111100|011 7ffffe3 [27] | |
| (222) |11111111|11111111|11111100|100 7ffffe4 [27] | |
| (223) |11111111|11111111|11111100|101 7ffffe5 [27] | |
| (224) |11111111|11111110|1100 fffec [20] | |
| (225) |11111111|11111111|11110011 fffff3 [24] | |
| (226) |11111111|11111110|1101 fffed [20] | |
| (227) |11111111|11111111|00110 1fffe6 [21] | |
| (228) |11111111|11111111|101001 3fffe9 [22] | |
| (229) |11111111|11111111|00111 1fffe7 [21] | |
| (230) |11111111|11111111|01000 1fffe8 [21] | |
| (231) |11111111|11111111|1110011 7ffff3 [23] | |
| (232) |11111111|11111111|101010 3fffea [22] | |
| (233) |11111111|11111111|101011 3fffeb [22] | |
| (234) |11111111|11111111|11110111|0 1ffffee [25] | |
| (235) |11111111|11111111|11110111|1 1ffffef [25] | |
| (236) |11111111|11111111|11110100 fffff4 [24] | |
| (237) |11111111|11111111|11110101 fffff5 [24] | |
| (238) |11111111|11111111|11111010|10 3ffffea [26] | |
| (239) |11111111|11111111|1110100 7ffff4 [23] | |
| (240) |11111111|11111111|11111010|11 3ffffeb [26] | |
| (241) |11111111|11111111|11111100|110 7ffffe6 [27] | |
| (242) |11111111|11111111|11111011|00 3ffffec [26] | |
| (243) |11111111|11111111|11111011|01 3ffffed [26] | |
| (244) |11111111|11111111|11111100|111 7ffffe7 [27] | |
| (245) |11111111|11111111|11111101|000 7ffffe8 [27] | |
| (246) |11111111|11111111|11111101|001 7ffffe9 [27] | |
| (247) |11111111|11111111|11111101|010 7ffffea [27] | |
| (248) |11111111|11111111|11111101|011 7ffffeb [27] | |
| (249) |11111111|11111111|11111111|1110 ffffffe [28] | |
| (250) |11111111|11111111|11111101|100 7ffffec [27] | |
| (251) |11111111|11111111|11111101|101 7ffffed [27] | |
| (252) |11111111|11111111|11111101|110 7ffffee [27] | |
| (253) |11111111|11111111|11111101|111 7ffffef [27] | |
| | |
| | |
| | |
|Peon & Ruellan Standards Track [Page 32] | |
| | |
| | |
|RFC 7541 HPACK May 2015 | |
| | |
| | |
| (254) |11111111|11111111|11111110|000 7fffff0 [27] | |
| (255) |11111111|11111111|11111011|10 3ffffee [26] | |
| EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]""".stripMargin | |
} | |
object Helpers { | |
implicit class RichString(val s: String) extends AnyVal { | |
def hexByteArray: Array[Byte] = | |
s.filter(_.isLetterOrDigit) | |
.grouped(2) | |
.map(x => java.lang.Integer.parseInt(x.mkString, 16).toByte) | |
.toArray | |
def hexByteString: ByteString = ByteString(hexByteArray) | |
} | |
implicit class RichByteArray(val ba: Array[Byte]) extends AnyVal { | |
def hexString: String = "[" + ba.map(_ formatted "%02X").mkString(" ") + "]" | |
} | |
implicit class RichRandom(val random: Random) extends AnyVal { | |
def nextByteString(size: Int): ByteString = { | |
val res = new Array[Byte](size) | |
random.nextBytes(res) | |
ByteString(res) | |
} | |
} | |
val hkdf = HKDF("HmacSHA256") | |
// https://tools.ietf.org/html/rfc8446#section-7.1 | |
def hkdfExpandLabel(secret: Array[Byte], label: String, context: Array[Byte], length: Int): Array[Byte] = { | |
val lengthBytes = ByteString((length >> 8).toByte, (length & 0xff).toByte) | |
def lengthPrefixed(bytes: ByteString): ByteString = { | |
require(bytes.length < 256) | |
ByteString(bytes.length.toByte) ++ bytes | |
} | |
val hkdfLabel = lengthBytes ++ lengthPrefixed(ByteString(s"tls13 $label", "ASCII")) ++ lengthPrefixed(ByteString(context)) | |
hkdf.expand(secret, hkdfLabel.toArray, length) | |
} | |
} | |
class HKDF(mac: Mac) { | |
def extract(salt: Array[Byte], inputKeyingMaterial: Array[Byte]): Array[Byte] = | |
hash( | |
if (salt.isEmpty) new Array[Byte](mac.getMacLength) else salt, | |
inputKeyingMaterial | |
) | |
private def hash(key: Array[Byte], data: Array[Byte]): Array[Byte] = { | |
mac.init(new SecretKeySpec(key, mac.getAlgorithm)) | |
mac.doFinal(data) | |
} | |
def expand(pseudoRandomKey: Array[Byte], info: Array[Byte], length: Int): Array[Byte] = { | |
require(length <= 255 * mac.getMacLength) | |
val okm = new Array[Byte](length) | |
val counter = new Array[Byte](1) | |
def step(cur: Array[Byte], round: Int): Unit = { | |
counter(0) = round.toByte | |
val tNext = hash(pseudoRandomKey, cur ++ info ++ counter) // FIXME: avoid copying | |
val offset = (round - 1) * mac.getMacLength | |
val remaining = length - offset | |
val toCopy = math.min(remaining, tNext.length) | |
println(s"length: $length round: $round offset: $offset rem: $remaining toCopy $toCopy") | |
System.arraycopy(tNext, 0, okm, offset, toCopy) | |
if (offset + mac.getMacLength < length) step(tNext, round + 1) | |
} | |
step(Array.emptyByteArray, 1) | |
okm | |
} | |
} | |
object HKDF { | |
def apply(algorithmName: String): HKDF = new HKDF(Mac.getInstance(algorithmName)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment