[ 
https://issues.apache.org/jira/browse/DRILL-8556?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18122118#comment-18122118
 ] 

ASF GitHub Bot commented on DRILL-8556:
---------------------------------------

github-advanced-security[bot] commented on code in PR #3087:
URL: https://github.com/apache/drill/pull/3087#discussion_r4170174711


##########
contrib/format-pcapng/src/main/java/org/apache/drill/exec/store/pcap/protocol/quic/QuicParser.java:
##########
@@ -0,0 +1,347 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ * http://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 org.apache.drill.exec.store.pcap.protocol.quic;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.drill.exec.store.pcap.protocol.DecoderContext;
+import org.apache.drill.exec.store.pcap.protocol.tls.TlsHello;
+import org.apache.drill.exec.store.pcap.protocol.tls.TlsParser;
+
+/**
+ * Parses a QUIC long-header packet (RFC 9000) and, for a version 1 Initial 
(RFC 9001), decrypts it with the
+ * Initial keys derived from the Destination Connection ID and the published 
salt (section 5.2). Those keys use
+ * only public values carried in the packet, so the TLS ClientHello inside can 
be read by passive inspection.
+ * No user traffic is decrypted: later packets use keys negotiated by the 
handshake, which this never sees.
+ */
+public final class QuicParser {
+  /** The version 1 Initial salt, RFC 9001 section 5.2. */
+  private static final byte[] INITIAL_SALT_V1 = {
+      0x38, 0x76, 0x2c, (byte) 0xf7, (byte) 0xf5, 0x59, 0x34, (byte) 0xb3, 
0x4d, 0x17,
+      (byte) 0x9a, (byte) 0xe6, (byte) 0xa4, (byte) 0xc8, 0x0c, (byte) 0xad, 
(byte) 0xcc, (byte) 0xbb,
+      0x7f, 0x0a};
+  private static final long VERSION_1 = 0x00000001L;
+  private static final int MAX_CID = 20;
+  private static final int SAMPLE_LENGTH = 16;
+  private static final int GCM_TAG_BITS = 128;
+  private static final int MAX_CRYPTO = 65536;
+  private static final int MAX_FRAMES = 4096;
+  private static final String[] PACKET_TYPES = {"initial", "0rtt", 
"handshake", "retry"};
+  private static final char[] HEX = "0123456789abcdef".toCharArray();
+
+  private QuicParser() {
+  }
+
+  /**
+   * @return null if the payload is not a QUIC long-header packet; a 
QuicInitial otherwise
+   * @throws IllegalArgumentException if the payload is a version 1 Initial 
but malformed or cannot be decrypted
+   */
+  public static QuicInitial parse(byte[] p, DecoderContext context) {
+    if (p == null || p.length < 6) {
+      return null;
+    }
+    int b0 = p[0] & 0xFF;
+    if ((b0 & 0x80) == 0) {
+      // Short header (1-RTT): not a handshake packet, nothing to inspect
+      return null;
+    }
+    long version = u32(p, 1);
+    int pos = 5;
+    int dcidLen = p[pos++] & 0xFF;
+    if (dcidLen > MAX_CID || pos + dcidLen > p.length) {
+      return null;
+    }
+    byte[] dcid = Arrays.copyOfRange(p, pos, pos + dcidLen);
+    pos += dcidLen;
+    if (pos >= p.length) {
+      return null;
+    }
+    int scidLen = p[pos++] & 0xFF;
+    if (scidLen > MAX_CID || pos + scidLen > p.length) {
+      return null;
+    }
+    byte[] scid = Arrays.copyOfRange(p, pos, pos + scidLen);
+    pos += scidLen;
+
+    QuicInitial q = new QuicInitial();
+    q.version = hex(p, 1, 4);
+    q.dcid = dcidLen == 0 ? null : hex(dcid, 0, dcidLen);
+    q.scid = scidLen == 0 ? null : hex(scid, 0, scidLen);
+    if (version == 0) {
+      q.packetType = "version_negotiation";
+      return q;
+    }
+    if (version != VERSION_1) {
+      // Another QUIC version: the Initial keys and header format may differ, 
so stop without decrypting
+      return q;
+    }
+    if ((b0 & 0x40) == 0) {
+      // The fixed bit is clear: not a valid QUIC version 1 packet
+      return null;
+    }
+    int type = (b0 >> 4) & 0x03;
+    q.packetType = PACKET_TYPES[type];
+    if (type != 0) {
+      // Only the Initial packet is decrypted; 0-RTT, Handshake and Retry are 
reported but not decrypted
+      return q;
+    }
+    // From here the packet is confidently a QUIC version 1 Initial: problems 
are reported, not swallowed
+    decryptInitial(p, pos, b0, dcid, q, context);
+    return q;
+  }
+
+  private static void decryptInitial(byte[] p, int pos, int b0, byte[] dcid, 
QuicInitial q,
+                                     DecoderContext context) {
+    long[] token = varint(p, pos);
+    pos = (int) token[1];
+    if (token[0] > p.length - pos) {
+      throw new IllegalArgumentException("token length " + token[0] + " 
exceeds the packet");
+    }
+    pos += (int) token[0];
+    long[] lengthField = varint(p, pos);
+    pos = (int) lengthField[1];
+    long length = lengthField[0];
+    int pnOffset = pos;
+    if (length < 1 || length > p.length - pnOffset) {
+      throw new IllegalArgumentException("packet length " + length + " exceeds 
the datagram");
+    }
+    if (pnOffset + 4 + SAMPLE_LENGTH > p.length) {
+      throw new IllegalArgumentException("truncated: no header protection 
sample");
+    }
+
+    byte[] secret = clientInitialSecret(dcid);
+    byte[] key = expandLabel(secret, "quic key", 16);
+    byte[] iv = expandLabel(secret, "quic iv", 12);
+    byte[] headerProtection = expandLabel(secret, "quic hp", 16);
+
+    byte[] sample = Arrays.copyOfRange(p, pnOffset + 4, pnOffset + 4 + 
SAMPLE_LENGTH);
+    byte[] mask = aesEcb(headerProtection, sample);
+    int firstByte = b0 ^ (mask[0] & 0x0F);
+    int pnLength = (firstByte & 0x03) + 1;
+    if (pnLength > length) {
+      throw new IllegalArgumentException("packet number length " + pnLength + 
" exceeds the packet");
+    }
+    byte[] header = Arrays.copyOfRange(p, 0, pnOffset + pnLength);
+    header[0] = (byte) firstByte;
+    long packetNumber = 0;
+    for (int i = 0; i < pnLength; i++) {
+      int value = (p[pnOffset + i] ^ mask[1 + i]) & 0xFF;
+      header[pnOffset + i] = (byte) value;
+      packetNumber = (packetNumber << 8) | value;
+    }
+
+    byte[] nonce = iv.clone();
+    for (int i = 0; i < 8; i++) {
+      nonce[nonce.length - 1 - i] ^= (packetNumber >>> (8 * i)) & 0xFF;
+    }
+    byte[] ciphertext = Arrays.copyOfRange(p, pnOffset + pnLength, pnOffset + 
(int) length);
+    byte[] plaintext;
+    try {
+      plaintext = aesGcmDecrypt(key, nonce, header, ciphertext);
+    } catch (GeneralSecurityException e) {
+      throw new IllegalArgumentException("AEAD authentication failed");
+    }
+
+    byte[] crypto = reassembleCrypto(plaintext, context);
+    if (crypto == null) {
+      return;
+    }
+    TlsHello hello = TlsParser.parseHandshake(crypto, 0, true, context);
+    if (hello != null) {
+      q.sni = hello.sni;
+      q.alpn = hello.alpn;
+      q.supportedVersions = hello.supportedVersions;
+      q.cipherSuites = hello.cipherSuites;
+      q.ja4 = hello.ja4;
+    }
+  }
+
+  /** Reassembles CRYPTO frames of a decrypted Initial into one buffer; other 
frame types are skipped. */
+  private static byte[] reassembleCrypto(byte[] p, DecoderContext context) {
+    byte[] buffer = null;
+    int end = 0;
+    int pos = 0;
+    int frames = 0;
+    try {
+      while (pos < p.length && frames++ < MAX_FRAMES) {
+        long[] frameType = varint(p, pos);
+        pos = (int) frameType[1];
+        long type = frameType[0];
+        if (type == 0x00 || type == 0x01) {
+          // PADDING or PING: no body
+          continue;
+        }
+        if (type == 0x02 || type == 0x03) {
+          pos = skipAck(p, pos, type == 0x03);
+          continue;
+        }
+        if (type != 0x06) {
+          // An unexpected frame type: stop and keep whatever CRYPTO was 
already collected
+          break;
+        }
+        long[] offset = varint(p, pos);
+        pos = (int) offset[1];
+        long[] len = varint(p, pos);
+        pos = (int) len[1];
+        if (len[0] > p.length - pos) {
+          break;
+        }
+        int segmentEnd = (int) Math.min((long) MAX_CRYPTO, offset[0] + len[0]);
+        if (offset[0] < MAX_CRYPTO && segmentEnd > offset[0]) {
+          if (buffer == null) {
+            buffer = new byte[MAX_CRYPTO];
+          }
+          System.arraycopy(p, pos, buffer, (int) offset[0], segmentEnd - (int) 
offset[0]);
+          end = Math.max(end, segmentEnd);
+        }
+        if (offset[0] + len[0] > MAX_CRYPTO) {
+          context.warn("CRYPTO data truncated to " + MAX_CRYPTO + " bytes");
+        }
+        pos += (int) len[0];
+      }
+    } catch (IllegalArgumentException e) {
+      // A malformed frame in the decrypted payload: keep the CRYPTO bytes 
gathered so far
+    }
+    return buffer == null ? null : Arrays.copyOf(buffer, end);
+  }
+
+  /** Skips an ACK frame, returning the offset just past it. */
+  private static int skipAck(byte[] p, int pos, boolean ecn) {
+    pos = (int) varint(p, pos)[1];                 // largest acknowledged
+    pos = (int) varint(p, pos)[1];                 // ack delay
+    long[] rangeCount = varint(p, pos);
+    pos = (int) rangeCount[1];
+    pos = (int) varint(p, pos)[1];                 // first ack range
+    for (long i = 0; i < rangeCount[0] && i < MAX_FRAMES; i++) {
+      pos = (int) varint(p, pos)[1];               // gap
+      pos = (int) varint(p, pos)[1];               // ack range length
+    }
+    if (ecn) {
+      pos = (int) varint(p, pos)[1];               // ect0
+      pos = (int) varint(p, pos)[1];               // ect1
+      pos = (int) varint(p, pos)[1];               // ecn-ce
+    }
+    return pos;
+  }
+
+  private static byte[] clientInitialSecret(byte[] dcid) {
+    byte[] initialSecret = hkdfExtract(INITIAL_SALT_V1, dcid);
+    return expandLabel(initialSecret, "client in", 32);
+  }
+
+  private static byte[] hkdfExtract(byte[] salt, byte[] ikm) {
+    return hmacSha256(salt, ikm);
+  }
+
+  /** HKDF-Expand-Label with an empty context, RFC 8446 section 7.1. */
+  private static byte[] expandLabel(byte[] secret, String label, int length) {
+    byte[] fullLabel = ("tls13 " + label).getBytes(StandardCharsets.US_ASCII);
+    byte[] info = new byte[2 + 1 + fullLabel.length + 1];
+    info[0] = (byte) (length >> 8);
+    info[1] = (byte) length;
+    info[2] = (byte) fullLabel.length;
+    System.arraycopy(fullLabel, 0, info, 3, fullLabel.length);
+    info[info.length - 1] = 0;
+    return hkdfExpand(secret, info, length);
+  }
+
+  private static byte[] hkdfExpand(byte[] prk, byte[] info, int length) {
+    byte[] out = new byte[length];
+    byte[] t = new byte[0];
+    int pos = 0;
+    byte counter = 1;
+    while (pos < length) {
+      byte[] input = new byte[t.length + info.length + 1];
+      System.arraycopy(t, 0, input, 0, t.length);
+      System.arraycopy(info, 0, input, t.length, info.length);
+      input[input.length - 1] = counter;
+      t = hmacSha256(prk, input);
+      int n = Math.min(t.length, length - pos);
+      System.arraycopy(t, 0, out, pos, n);
+      pos += n;
+      counter++;
+    }
+    return out;
+  }
+
+  private static byte[] hmacSha256(byte[] key, byte[] data) {
+    try {
+      Mac mac = Mac.getInstance("HmacSHA256");
+      mac.init(new SecretKeySpec(key.length == 0 ? new byte[1] : key, 
"HmacSHA256"));
+      return mac.doFinal(data);
+    } catch (GeneralSecurityException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private static byte[] aesEcb(byte[] key, byte[] data) {
+    try {
+      Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");

Review Comment:
   ## CodeQL / Use of a broken or risky cryptographic algorithm
   
   Cryptographic algorithm [AES/ECB/NoPadding](1) is insecure. ECB mode, as in 
AES/ECB/NoPadding for example, is vulnerable to replay and other attacks. 
Consider using GCM instead.
   
   [Show more 
details](https://github.com/apache/drill/security/code-scanning/88)





> Various PCAP and PCAP-NG Improvements
> -------------------------------------
>
>                 Key: DRILL-8556
>                 URL: https://issues.apache.org/jira/browse/DRILL-8556
>             Project: Apache Drill
>          Issue Type: Improvement
>          Components: Format - PCAP
>    Affects Versions: 1.22.0
>            Reporter: Charles Givre
>            Assignee: Charles Givre
>            Priority: Major
>             Fix For: 1.23.0
>
>
> See PR for full details.  This PR contains several fixes and performance 
> enhancements to the PCAP reader. 



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to