Skip to content

Instantly share code, notes, and snippets.

@Genzer
Last active June 27, 2025 07:52
Show Gist options
  • Select an option

  • Save Genzer/034e9de6b0020de5269ae15c34139109 to your computer and use it in GitHub Desktop.

Select an option

Save Genzer/034e9de6b0020de5269ae15c34139109 to your computer and use it in GitHub Desktop.
A simple implementation of time-based nonce

Time-based nonce

This is a simple time-based nonce that you can used in various situations. The nonce is designed to be time-based so you can use it with a short lifetime (e.g. 5 or 10 seconds).

Design decisions

TimestampedNonce uses 192 bits with:

  • First 64 bits for the expiration timestamp, milliseconds since EPOCH.
  • The next 128 bits is for entropy. This is secure enough in this situation.

TimestampedNonce.toString() returns a base64-urlencoded string of the entire 192-bit octets.

SignedTimestampedNonce is a TimestampedNonce signed with HMAC-SHA256.

An example use case

  • The Server creates a SignedTimestampedNonce. The Server uses SignedTimestmapedNonce#signature() to set a cookie nonce_signature, then returns the SignedTimestampedNonce#signedNonce().toString() to the Client.
  • The Client must attach the signedNonce in its URL Parameter or the payload. This also works for CRSF protection.
  • The Client must send along with the cookie nonce_signature.
  • The Server verifies both the signedNonce and nonce_signature. If both matches, proceed.

Usage

jshell> /open TimestampedNonce.java

jshell> var nonce = TimestampedNonce.expiresAt(Instant.now().plusSeconds(10));
nonce ==> AAABl6_3SdD2odb1QJjRSPHSAeRV31EV

jshell> var hmacKey = new byte[32];
hmacKey ==> byte[32] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ... , 0, 0, 0, 0, 0, 0, 0, 0 }

jshell> new SecureRandom().nextBytes(hmacKey);

jshell> var signedNonce = TimestampedNonce.SignedTimestampedNonce.sign(nonce, hmacKey);
signedNonce ==> AAABl6_3SdD2odb1QJjRSPHSAeRV31EV:HMAC-SHA256:-QmQbeudWd ... UIMqZaNfZCw3J0bcUcak_r9Ak=

jshell> signedNonce.signedNonce().toString();
$37 ==> "AAABl6_3SdD2odb1QJjRSPHSAeRV31EV"

jshell> signedNonce.signature()
$38 ==> "-QmQbeudWdHkDzakn0UIMqZaNfZCw3J0bcUcak_r9Ak="

jshell> var recreate = TimestampedNonce.SignedTimestampedNonce.verify("AAABl6_3SdD2odb1QJjRSPHSAeRV31EV", "-QmQbeudWdHkDzakn0UIMqZaNfZCw3J0bcUcak_r9Ak=", hmacKey);
recreate ==> AAABl6_3SdD2odb1QJjRSPHSAeRV31EV:HMAC-SHA256:-QmQbeudWd ... UIMqZaNfZCw3J0bcUcak_r9Ak=

jshell>
import java.security.SecureRandom;
import javax.crypto.Mac;
import javax.crypto.spec.*;
import java.security.*;
import java.time.Instant;
import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.HexFormat;
public class TimestampedNonce {
private static final int RANDOM_BITS_SIZE = 128 / 8;
private static final int TIMESTAMPT_BITS_SIZE = 64 / 8; /* a long */
private static final int NONCE_SIZE = RANDOM_BITS_SIZE + TIMESTAMPT_BITS_SIZE;
/* Stores the timestamp to avoid extracting again */
private final long expirationInEpochMillis;
private final String nonceEncoded;
public static TimestampedNonce expiresAt(Instant expiration) {
long expirationInEpochMillis = expiration.toEpochMilli();
var nonceOctets = new byte[NONCE_SIZE];
new SecureRandom().nextBytes(nonceOctets);
var nonceBuffer = ByteBuffer.wrap(nonceOctets);
nonceBuffer.putLong(expirationInEpochMillis);
return new TimestampedNonce(expirationInEpochMillis, nonceOctets);
}
public static TimestampedNonce parse(String timestampedNonceLiteral) {
var decoded = Base64.getUrlDecoder().decode(timestampedNonceLiteral);
if (decoded.length != (NONCE_SIZE)) {
throw new IllegalArgumentException("Invalid decoded size: " + decoded.length + "/" + NONCE_SIZE);
}
var nonceBuffer = ByteBuffer.wrap(decoded);
long expirationInEpochMillis = nonceBuffer.getLong();
return new TimestampedNonce(expirationInEpochMillis, decoded);
}
private TimestampedNonce(long expirationInEpochMillis, byte[] rawNonce) {
this.expirationInEpochMillis = expirationInEpochMillis;
this.nonceEncoded = Base64.getUrlEncoder().encodeToString(rawNonce);
}
public Instant expiration() {
return Instant.ofEpochMilli(this.expirationInEpochMillis);
}
public boolean isExpired() {
return this.expiration().isBefore(Instant.now());
}
public String toString() {
return this.nonceEncoded;
}
private byte[] rawNonce() {
return Base64.getUrlDecoder().decode(this.nonceEncoded);
}
public static class SignedTimestampedNonce {
private static final String SINGING_ALGORITHM = "HmacSHA256";
private final TimestampedNonce timestampedNonce;
private final String hmacSignature;
public static SignedTimestampedNonce sign(TimestampedNonce nonce, byte[] key) {
var mac = createMac(key);
byte[] signatureOctets = mac.doFinal(nonce.rawNonce());
return new SignedTimestampedNonce(
nonce, Base64.getUrlEncoder().encodeToString(signatureOctets));
}
private static Mac createMac(byte[] key) {
try {
if (key.length < 32) {
throw new IllegalArgumentException("HmacSHA256 requires at least 256-bit key. Got " + key.length);
}
SecretKeySpec secretKeySpec = new SecretKeySpec(key, SINGING_ALGORITHM);
Mac mac = Mac.getInstance(SINGING_ALGORITHM);
mac.init(secretKeySpec);
return mac;
} catch (Exception any) {
throw new RuntimeException("Fail to create HmacSHA256", any);
}
}
public static SignedTimestampedNonce verify(String timestampedNonceLiteral, String signature, byte[] key) {
var unsureTimestampedNonce = TimestampedNonce.parse(timestampedNonceLiteral);
var signed = SignedTimestampedNonce.sign(unsureTimestampedNonce, key);
if (!signed.hmacSignature.equals(signature)) {
throw new IllegalArgumentException("Signature mismatch");
}
return signed;
}
private SignedTimestampedNonce(TimestampedNonce nonce, String signature) {
this.timestampedNonce = nonce;
this.hmacSignature = signature;
}
public TimestampedNonce signedNonce() {
return this.timestampedNonce;
}
public String signature() {
return this.hmacSignature;
}
public String toString() {
return this.timestampedNonce + ":HMAC-SHA256:" + this.hmacSignature;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment