View Javadoc
1   /*
2    * Copyright 2025 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.quic;
17  
18  import io.netty.util.internal.ObjectUtil;
19  
20  import java.nio.ByteBuffer;
21  import java.nio.ByteOrder;
22  
23  /**
24   * <a href="https://www.aumasson.jp/siphash/siphash.pdf">Siphash implementation</a>.
25   */
26  final class SipHash {
27  
28      static final int SEED_LENGTH = 16;
29  
30      // Make this class allocation free as soon as its constructed.
31      private final int compressionRounds;
32      private final int finalizationRounds;
33  
34      // As specified in https://www.aumasson.jp/siphash/siphash.pdf
35      private static final long INITIAL_STATE_V0 = 0x736f6d6570736575L; // "somepseu"
36      private static final long INITIAL_STATE_V1 = 0x646f72616e646f6dL; // "dorandom"
37      private static final long INITIAL_STATE_V2 = 0x6c7967656e657261L; // "lygenera"
38      private static final long INITIAL_STATE_V3 = 0x7465646279746573L;  // "tedbytes"
39  
40      private final long initialStateV0;
41      private final long initialStateV1;
42      private final long initialStateV2;
43      private final long initialStateV3;
44  
45      private long v0;
46      private long v1;
47      private long v2;
48      private long v3;
49  
50      SipHash(int compressionRounds, int finalizationRounds, byte[] seed) {
51          if (seed.length != SEED_LENGTH) {
52              throw new IllegalArgumentException("seed must be of length " + SEED_LENGTH);
53          }
54          this.compressionRounds = ObjectUtil.checkPositive(compressionRounds, "compressionRounds");
55          this.finalizationRounds = ObjectUtil.checkPositive(finalizationRounds, "finalizationRounds");
56  
57          // Wrap the seed to extract two longs that will be used to generate the initial state.
58          // Use little-endian as in the paper.
59          ByteBuffer keyBuffer = ByteBuffer.wrap(seed).order(ByteOrder.LITTLE_ENDIAN);
60          final long k0 = keyBuffer.getLong();
61          final long k1 = keyBuffer.getLong();
62  
63          initialStateV0 = INITIAL_STATE_V0 ^ k0;
64          initialStateV1 = INITIAL_STATE_V1 ^ k1;
65          initialStateV2 = INITIAL_STATE_V2 ^ k0;
66          initialStateV3 = INITIAL_STATE_V3 ^ k1;
67      }
68  
69      long macHash(ByteBuffer input) {
70          v0 = initialStateV0;
71          v1 = initialStateV1;
72          v2 = initialStateV2;
73          v3 = initialStateV3;
74          int remaining = input.remaining();
75          int position = input.position();
76          int len = remaining - (remaining % Long.BYTES);
77          boolean needsReverse = input.order() == ByteOrder.BIG_ENDIAN;
78          for (int offset = position; offset < len; offset +=  Long.BYTES) {
79              long m = input.getLong(offset);
80              if (needsReverse) {
81                  // We use little-endian as in the paper.
82                  m = Long.reverseBytes(m);
83              }
84              v3 ^= m;
85              for (int i = 0; i < compressionRounds; i++) {
86                  sipround();
87              }
88              v0 ^= m;
89          }
90  
91          // Get last bits.
92          final int left = remaining & (Long.BYTES - 1);
93          long b = (long) remaining << 56;
94          assert left < Long.BYTES;
95          switch (left) {
96              case 7:
97                  b |= (long) input.get(position + len + 6) << 48;
98              case 6:
99                  b |= (long) input.get(position + len + 5) << 40;
100             case 5:
101                 b |= (long) input.get(position + len + 4) << 32;
102             case 4:
103                 b |= (long) input.get(position + len + 3) << 24;
104             case 3:
105                 b |= (long) input.get(position + len + 2) << 16;
106             case 2:
107                 b |= (long) input.get(position + len + 1) << 8;
108             case 1:
109                 b |= input.get(position + len);
110                 break;
111             case 0:
112                 break;
113             default:
114                 throw new IllegalStateException("Unexpected value: " + left);
115         }
116 
117         v3 ^= b;
118         for (int i = 0; i < compressionRounds; i++) {
119             sipround();
120         }
121 
122         v0 ^= b;
123         v2 ^= 0xFF;
124         for (int i = 0; i < finalizationRounds; i++) {
125             sipround();
126         }
127 
128         return v0 ^ v1 ^ v2 ^ v3;
129     }
130 
131     private void sipround() {
132         v0 += v1;
133         v2 += v3;
134         v1 = Long.rotateLeft(v1, 13);
135         v3 = Long.rotateLeft(v3, 16);
136         v1 ^= v0;
137         v3 ^= v2;
138 
139         v0 = Long.rotateLeft(v0, 32);
140 
141         v2 += v1;
142         v0 += v3;
143         v1 = Long.rotateLeft(v1, 17);
144         v3 = Long.rotateLeft(v3, 21);
145         v1 ^= v2;
146         v3 ^= v0;
147 
148         v2 = Long.rotateLeft(v2, 32);
149     }
150 }