Skip to content

Commit ced68c8

Browse files
authored
Merge branch 'master' into add-array-rotation-utility
2 parents 71c4ff0 + 9d9e348 commit ced68c8

82 files changed

Lines changed: 11781 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
steps:
1111
- uses: actions/checkout@v7
1212
- name: Set up JDK
13-
uses: actions/setup-java@v6.0.0
13+
uses: actions/setup-java@v6.0.1
1414
with:
1515
java-version: 21
1616
distribution: 'temurin'

‎.github/workflows/codeql.yml‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@ jobs:
2424
uses: actions/checkout@v7
2525

2626
- name: Set up JDK
27-
uses: actions/setup-java@v6.0.0
27+
uses: actions/setup-java@v6.0.1
2828
with:
2929
java-version: 21
3030
distribution: 'temurin'
3131

3232
- name: Initialize CodeQL
33-
uses: github/codeql-action/init@v4.37.9
33+
uses: github/codeql-action/init@v4.38.2
3434
with:
3535
languages: 'java-kotlin'
3636

3737
- name: Build
3838
run: mvn --batch-mode --update-snapshots verify
3939

4040
- name: Perform CodeQL Analysis
41-
uses: github/codeql-action/analyze@v4.37.9
41+
uses: github/codeql-action/analyze@v4.38.2
4242
with:
4343
category: "/language:java-kotlin"
4444

@@ -55,12 +55,12 @@ jobs:
5555
uses: actions/checkout@v7
5656

5757
- name: Initialize CodeQL
58-
uses: github/codeql-action/init@v4.37.9
58+
uses: github/codeql-action/init@v4.38.2
5959
with:
6060
languages: 'actions'
6161

6262
- name: Perform CodeQL Analysis
63-
uses: github/codeql-action/analyze@v4.37.9
63+
uses: github/codeql-action/analyze@v4.38.2
6464
with:
6565
category: "/language:actions"
6666
...

‎.github/workflows/infer.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- uses: actions/checkout@v7
1919

2020
- name: Set up JDK
21-
uses: actions/setup-java@v6.0.0
21+
uses: actions/setup-java@v6.0.1
2222
with:
2323
java-version: 21
2424
distribution: 'temurin'

‎pom.xml‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@
4242
<dependency>
4343
<groupId>org.mockito</groupId>
4444
<artifactId>mockito-core</artifactId>
45-
<version>5.23.0</version>
45+
<version>5.24.0</version>
4646
<scope>test</scope>
4747
</dependency>
4848
<dependency>
4949
<groupId>org.apache.commons</groupId>
5050
<artifactId>commons-lang3</artifactId>
51-
<version>3.20.0</version>
51+
<version>3.21.0</version>
5252
</dependency>
5353
<dependency>
5454
<groupId>org.apache.commons</groupId>
@@ -61,15 +61,15 @@
6161
<plugins>
6262
<plugin>
6363
<artifactId>maven-surefire-plugin</artifactId>
64-
<version>3.5.6</version>
64+
<version>3.6.0</version>
6565
<configuration>
6666
<forkNode implementation="org.apache.maven.plugin.surefire.extensions.SurefireForkNodeFactory"/>
6767
</configuration>
6868
</plugin>
6969
<plugin>
7070
<groupId>org.apache.maven.plugins</groupId>
7171
<artifactId>maven-compiler-plugin</artifactId>
72-
<version>3.15.0</version>
72+
<version>3.16.0</version>
7373
<configuration>
7474
<release>21</release>
7575
<compilerArgs>
@@ -112,14 +112,14 @@
112112
<dependency>
113113
<groupId>com.puppycrawl.tools</groupId>
114114
<artifactId>checkstyle</artifactId>
115-
<version>14.0.0</version>
115+
<version>14.3.0</version>
116116
</dependency>
117117
</dependencies>
118118
</plugin>
119119
<plugin>
120120
<groupId>com.github.spotbugs</groupId>
121121
<artifactId>spotbugs-maven-plugin</artifactId>
122-
<version>4.10.4.0</version>
122+
<version>4.10.4.1</version>
123123
<configuration>
124124
<excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
125125
<includeTests>true</includeTests>
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.security.MessageDigest;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.SecureRandom;
6+
7+
/**
8+
* HORS (Hash to Obtain Random Subset) is a hash-based few-time signature scheme by Reyzin and
9+
* Reyzin (2002).
10+
*
11+
* <p>The private key consists of {@code t = 2^τ} random 32-byte secrets and the public key contains
12+
* their SHA-256 hashes. To sign, the SHA-256 digest of the message is split into {@code k} chunks of
13+
* {@code τ} bits (most significant bit first); each chunk is an index into the key, and the secrets
14+
* at those {@code k} indices form the signature. The verifier derives the same indices and checks
15+
* that each revealed secret hashes to the matching public-key value. The same index may appear more
16+
* than once, as in the original scheme. Signing is deterministic.
17+
*
18+
* <p>HORS is a few-time scheme: each signature reveals {@code k} secrets, so after {@code r}
19+
* signatures an attacker knows at most {@code r·k} of them. If all indices of another message fall
20+
* into that set, its signature can be forged, so security decreases with every signature.
21+
*
22+
* <p>HORST compresses the large public key with a Merkle tree. FORS, used in SPHINCS+ and SLH-DSA
23+
* (FIPS 205), uses a forest of trees instead, which also removes the repeated-index weakness. This
24+
* implementation is educational and must not be used in production.
25+
*
26+
* <p>References: <a href="https://eprint.iacr.org/2002/014">Reyzin and Reyzin, Better than BiBa</a>,
27+
* <a href="https://en.wikipedia.org/wiki/Hash-based_cryptography">Wikipedia: Hash-based cryptography</a>
28+
*
29+
* @author dilaraacetin
30+
* @see LamportSignature
31+
* @see WinternitzSignature
32+
* @see MerkleSignatureScheme
33+
*/
34+
public final class HorsSignature {
35+
36+
// SHA-256 output length in bytes
37+
private static final int HASH_BYTES = 32;
38+
private static final int DIGEST_BITS = 256;
39+
private static final int MIN_T = 16;
40+
private static final int MAX_T = 65536;
41+
private static final int DEFAULT_T = 1024;
42+
private static final int DEFAULT_K = 16;
43+
44+
private final int k;
45+
private final int tau;
46+
private final byte[][] privateKey;
47+
private final byte[][] publicKey;
48+
49+
/**
50+
* Generates a new key pair with {@code t = 1024} and {@code k = 16}.
51+
*/
52+
public HorsSignature() {
53+
this(DEFAULT_T, DEFAULT_K);
54+
}
55+
56+
/**
57+
* Generates a new key pair.
58+
*
59+
* @param t the number of secrets; a power of two between 16 and 65536
60+
* @param k the number of secrets revealed per signature; {@code k·log2(t)} must not exceed 256
61+
* @throws IllegalArgumentException if {@code t} or {@code k} is not supported
62+
*/
63+
public HorsSignature(int t, int k) {
64+
this.tau = tauOf(t);
65+
validateK(k, tau);
66+
this.k = k;
67+
SecureRandom secureRandom = new SecureRandom();
68+
privateKey = new byte[t][HASH_BYTES];
69+
publicKey = new byte[t][];
70+
for (int i = 0; i < t; i++) {
71+
secureRandom.nextBytes(privateKey[i]);
72+
publicKey[i] = hash(privateKey[i]);
73+
}
74+
}
75+
76+
/**
77+
* Returns a copy of the public key.
78+
*
79+
* @return {@code t} hash values, each 32 bytes long
80+
*/
81+
public byte[][] getPublicKey() {
82+
return deepCopy(publicKey);
83+
}
84+
85+
/**
86+
* Returns the number of secrets revealed per signature. Like {@code t}, it is part of the public
87+
* key and is needed for verification.
88+
*
89+
* @return the parameter {@code k}
90+
*/
91+
public int getK() {
92+
return k;
93+
}
94+
95+
/**
96+
* Signs a message. Every signature reveals {@code k} secrets, so a key pair should only sign a
97+
* few messages.
98+
*
99+
* @param message the message to sign
100+
* @return the signature: {@code k} secrets of 32 bytes each
101+
* @throws IllegalArgumentException if the message is null
102+
*/
103+
public byte[][] sign(byte[] message) {
104+
if (message == null) {
105+
throw new IllegalArgumentException("message must not be null");
106+
}
107+
int[] indices = messageIndices(hash(message), k, tau);
108+
byte[][] signature = new byte[k][];
109+
for (int j = 0; j < k; j++) {
110+
signature[j] = privateKey[indices[j]].clone();
111+
}
112+
return signature;
113+
}
114+
115+
/**
116+
* Verifies a signature against a public key. {@code t} is taken from the public key length.
117+
* {@code k} is part of the public key and must be supplied by the verifier: taking it from the
118+
* signature would let an attacker submit a shorter signature that reveals fewer secrets.
119+
*
120+
* @param message the signed message
121+
* @param signature the signature to check
122+
* @param publicKey the public key of the signer
123+
* @param k the number of secrets per signature used by the signer (see {@link #getK()})
124+
* @return true if the signature is valid for the message and public key, false otherwise
125+
* @throws IllegalArgumentException if an argument is null, the public key length is not a
126+
* supported {@code t}, {@code k} is not supported for this {@code t}, a value is not 32
127+
* bytes long, or the signature does not contain exactly {@code k} values
128+
*/
129+
public static boolean verify(byte[] message, byte[][] signature, byte[][] publicKey, int k) {
130+
if (message == null) {
131+
throw new IllegalArgumentException("message must not be null");
132+
}
133+
validateValues(publicKey, "publicKey");
134+
validateValues(signature, "signature");
135+
int tau = tauOf(publicKey.length);
136+
validateK(k, tau);
137+
if (signature.length != k) {
138+
throw new IllegalArgumentException("signature must contain exactly " + k + " values, got " + signature.length);
139+
}
140+
141+
int[] indices = messageIndices(hash(message), k, tau);
142+
for (int j = 0; j < k; j++) {
143+
if (!MessageDigest.isEqual(hash(signature[j]), publicKey[indices[j]])) {
144+
return false;
145+
}
146+
}
147+
return true;
148+
}
149+
150+
/**
151+
* Splits the first {@code k·τ} bits of a digest into {@code k} unsigned {@code τ}-bit indices,
152+
* most significant bit first. Shared by sign and verify.
153+
*/
154+
static int[] messageIndices(byte[] digest, int k, int tau) {
155+
if ((long) k * tau > digest.length * 8L) {
156+
throw new IllegalArgumentException("digest is too short for " + k + " indices of " + tau + " bits");
157+
}
158+
int[] indices = new int[k];
159+
for (int j = 0; j < k; j++) {
160+
int index = 0;
161+
for (int i = 0; i < tau; i++) {
162+
int bit = j * tau + i;
163+
// bit 0 is the most significant bit of digest[0]; & 0xFF reads the byte as unsigned
164+
index = (index << 1) | (((digest[bit / 8] & 0xFF) >> (7 - bit % 8)) & 1);
165+
}
166+
indices[j] = index;
167+
}
168+
return indices;
169+
}
170+
171+
private static int tauOf(int t) {
172+
if (t < MIN_T || t > MAX_T || (t & (t - 1)) != 0) {
173+
throw new IllegalArgumentException("t must be a power of two between " + MIN_T + " and " + MAX_T + ", got " + t);
174+
}
175+
return Integer.numberOfTrailingZeros(t);
176+
}
177+
178+
private static void validateK(int k, int tau) {
179+
// k > DIGEST_BITS / tau is the same as k * tau > DIGEST_BITS, but cannot overflow
180+
if (k < 1 || k > DIGEST_BITS / tau) {
181+
throw new IllegalArgumentException("k must be at least 1 and k * log2(t) at most " + DIGEST_BITS + ", got k = " + k);
182+
}
183+
}
184+
185+
private static void validateValues(byte[][] values, String name) {
186+
if (values == null) {
187+
throw new IllegalArgumentException(name + " must not be null");
188+
}
189+
for (byte[] value : values) {
190+
if (value == null || value.length != HASH_BYTES) {
191+
throw new IllegalArgumentException(name + " values must be exactly " + HASH_BYTES + " bytes long");
192+
}
193+
}
194+
}
195+
196+
private static byte[] hash(byte[] data) {
197+
try {
198+
return MessageDigest.getInstance("SHA-256").digest(data);
199+
} catch (NoSuchAlgorithmException e) {
200+
throw new AssertionError("SHA-256 is required by the Java SE specification", e);
201+
}
202+
}
203+
204+
private static byte[][] deepCopy(byte[][] values) {
205+
byte[][] copy = new byte[values.length][];
206+
for (int i = 0; i < values.length; i++) {
207+
copy[i] = values[i].clone();
208+
}
209+
return copy;
210+
}
211+
}

0 commit comments

Comments
 (0)