Skip to content

Commit 9e94464

Browse files
Merge branch 'master' into fix/rotated-binary-search-edge-cases
2 parents c08c7eb + 3f06708 commit 9e94464

67 files changed

Lines changed: 9999 additions & 48 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@v5.7.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@v5.7.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.7
33+
uses: github/codeql-action/init@v4.38.1
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.7
41+
uses: github/codeql-action/analyze@v4.38.1
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.7
58+
uses: github/codeql-action/init@v4.38.1
5959
with:
6060
languages: 'actions'
6161

6262
- name: Perform CodeQL Analysis
63-
uses: github/codeql-action/analyze@v4.37.7
63+
uses: github/codeql-action/analyze@v4.38.1
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@v5.7.0
21+
uses: actions/setup-java@v6.0.1
2222
with:
2323
java-version: 21
2424
distribution: 'temurin'

‎pom.xml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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.1.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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package com.thealgorithms.ciphers;
2+
3+
import java.security.MessageDigest;
4+
import java.security.NoSuchAlgorithmException;
5+
import java.security.SecureRandom;
6+
import java.util.Arrays;
7+
8+
/**
9+
* Lamport one-time signatures (OTS) are a hash-based post-quantum signature scheme.
10+
*
11+
* <p>The scheme is based on a private key consisting of 256 pairs of random 32-byte secrets,
12+
* one pair per bit position of a SHA-256 digest. The public key contains the SHA-256 hashes of
13+
* those secrets.
14+
*
15+
* <p>To sign a message, the algorithm hashes the message and reveals the secret corresponding to
16+
* each bit value in the digest. Verification recomputes the digest and checks that each revealed
17+
* secret hashes to the expected public-key value.
18+
*
19+
* <p>This implementation is educational and intentionally keeps dependencies minimal. Each key pair
20+
* is strictly one-time: signing a second message with the same key is rejected.
21+
*
22+
* <p>Reference: <a href="https://en.wikipedia.org/wiki/Lamport_signature">Wikipedia: Lamport signature</a>
23+
*/
24+
public final class LamportSignature {
25+
26+
private static final int DIGEST_BITS = 256;
27+
private static final int SECRET_BYTES = 32;
28+
private static final int NUM_VALUES = 2;
29+
30+
private LamportSignature() {
31+
}
32+
33+
/**
34+
* A Lamport key pair containing both the private and public material.
35+
*/
36+
public static final class KeyPair {
37+
private final byte[][][] privateKey;
38+
private final byte[][][] publicKey;
39+
private boolean used;
40+
41+
private KeyPair(byte[][][] privateKey, byte[][][] publicKey) {
42+
this.privateKey = privateKey;
43+
this.publicKey = publicKey;
44+
}
45+
46+
/**
47+
* Signs a message using this key pair.
48+
*
49+
* @param message the message to sign
50+
* @return the signature bytes; exactly 256 secret values ordered by digest bit positions
51+
* @throws IllegalArgumentException if the message is null
52+
* @throws IllegalStateException if this key pair has already been used to sign a message
53+
*/
54+
public byte[] sign(byte[] message) {
55+
if (message == null) {
56+
throw new IllegalArgumentException("message must not be null");
57+
}
58+
if (used) {
59+
throw new IllegalStateException("This Lamport key pair can only sign one message");
60+
}
61+
used = true;
62+
63+
byte[] digest = sha256(message);
64+
byte[] signature = new byte[DIGEST_BITS * SECRET_BYTES];
65+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
66+
int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01;
67+
byte[] secret = privateKey[bit][value];
68+
System.arraycopy(secret, 0, signature, bit * SECRET_BYTES, SECRET_BYTES);
69+
}
70+
return signature;
71+
}
72+
73+
/**
74+
* Verifies a message signature against this public key.
75+
*
76+
* @param message the message to verify
77+
* @param signature the signature to verify
78+
* @return true if the signature is valid for the provided message and public key
79+
* @throws IllegalArgumentException if message or signature is null or malformed
80+
*/
81+
public boolean verify(byte[] message, byte[] signature) {
82+
if (message == null) {
83+
throw new IllegalArgumentException("message must not be null");
84+
}
85+
if (signature == null) {
86+
throw new IllegalArgumentException("signature must not be null");
87+
}
88+
if (signature.length != DIGEST_BITS * SECRET_BYTES) {
89+
throw new IllegalArgumentException("signature length must be exactly 8192 bytes");
90+
}
91+
92+
byte[] digest = sha256(message);
93+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
94+
int value = (digest[bit / 8] >> (7 - (bit % 8))) & 0x01;
95+
byte[] revealedSecret = Arrays.copyOfRange(signature, bit * SECRET_BYTES, (bit + 1) * SECRET_BYTES);
96+
byte[] expectedHash = publicKey[bit][value];
97+
byte[] actualHash = sha256(revealedSecret);
98+
if (!MessageDigest.isEqual(actualHash, expectedHash)) {
99+
return false;
100+
}
101+
}
102+
return true;
103+
}
104+
105+
public byte[][][] getPrivateKey() {
106+
return privateKey;
107+
}
108+
109+
public byte[][][] getPublicKey() {
110+
return publicKey;
111+
}
112+
}
113+
114+
/**
115+
* Generates a new Lamport key pair.
116+
*
117+
* @return a fresh Lamport key pair
118+
*/
119+
public static KeyPair generateKeyPair() {
120+
SecureRandom secureRandom = new SecureRandom();
121+
byte[][][] privateKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES];
122+
byte[][][] publicKey = new byte[DIGEST_BITS][NUM_VALUES][SECRET_BYTES];
123+
124+
for (int bit = 0; bit < DIGEST_BITS; bit++) {
125+
for (int value = 0; value < NUM_VALUES; value++) {
126+
secureRandom.nextBytes(privateKey[bit][value]);
127+
publicKey[bit][value] = sha256(privateKey[bit][value]);
128+
}
129+
}
130+
return new KeyPair(privateKey, publicKey);
131+
}
132+
133+
static byte[] sha256(byte[] input) {
134+
try {
135+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
136+
return digest.digest(input);
137+
} catch (NoSuchAlgorithmException e) {
138+
throw new AssertionError("SHA-256 is required by the Java SE specification", e);
139+
}
140+
}
141+
142+
static byte[] sha256(byte[] message, int offset, int length) {
143+
return sha256(Arrays.copyOfRange(message, offset, offset + length));
144+
}
145+
}

0 commit comments

Comments
 (0)