Skip to content

Commit cc1bd68

Browse files
committed
fix: safely handle nonces as 64 bit uints
1 parent 01c490e commit cc1bd68

File tree

3 files changed

+25
-9
lines changed

3 files changed

+25
-9
lines changed

src/@types/basic.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,4 @@ export type bytes = Uint8Array
22
export type bytes32 = Uint8Array
33
export type bytes16 = Uint8Array
44

5-
export type uint32 = number
65
export type uint64 = number

src/@types/handshake.d.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { bytes, bytes32, uint32, uint64 } from './basic'
1+
import { bytes, bytes32, uint64 } from './basic'
22
import { KeyPair } from './libp2p'
33

44
export type Hkdf = [bytes, bytes, bytes]
@@ -11,7 +11,9 @@ export interface MessageBuffer {
1111

1212
export interface CipherState {
1313
k: bytes32
14-
n: uint32
14+
// For performance reasons, the nonce is represented as a JS `number`
15+
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
16+
n: uint64
1517
}
1618

1719
export interface SymmetricState {

src/handshakes/abstract-handshake.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
55
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
66
import { fromString as uint8ArrayFromString } from 'uint8arrays'
77

8-
import { bytes, bytes32, uint32 } from '../@types/basic'
8+
import { bytes, bytes32, uint64 } from '../@types/basic'
99
import { CipherState, MessageBuffer, SymmetricState } from '../@types/handshake'
1010
import { getHkdf } from '../utils'
1111
import { logger } from '../logger'
1212

1313
export const MIN_NONCE = 0
14+
// For performance reasons, the nonce is represented as a JS `number`
15+
// JS `number` can only safely represent integers up to 2 ** 53 - 1
16+
// This is a slight deviation from the noise spec, which describes the max nonce as 2 ** 64 - 2
17+
// The effect is that this implementation will need a new handshake to be performed after fewer messages are exchanged than other implementations with full uint64 nonces.
18+
// 2 ** 53 - 1 is still a large number of messages, so the practical effect of this is negligible.
19+
export const MAX_NONCE = Number.MAX_SAFE_INTEGER
20+
21+
const ERR_MAX_NONCE = 'Cipherstate has reached maximum n, a new handshake must be performed'
1422

1523
export abstract class AbstractHandshake {
1624
public encryptWithAd (cs: CipherState, ad: Uint8Array, plaintext: Uint8Array): bytes {
@@ -32,7 +40,7 @@ export abstract class AbstractHandshake {
3240
return !this.isEmptyKey(cs.k)
3341
}
3442

35-
protected setNonce (cs: CipherState, nonce: uint32): void {
43+
protected setNonce (cs: CipherState, nonce: uint64): void {
3644
cs.n = nonce
3745
}
3846

@@ -45,18 +53,22 @@ export abstract class AbstractHandshake {
4553
return uint8ArrayEquals(emptyKey, k)
4654
}
4755

48-
protected incrementNonce (n: uint32): uint32 {
56+
protected incrementNonce (n: uint64): uint64 {
4957
return n + 1
5058
}
5159

52-
protected nonceToBytes (n: uint32): bytes {
60+
protected nonceToBytes (n: uint64): bytes {
61+
// Even though we're treating the nonce as 8 bytes, RFC7539 specifies 12 bytes for a nonce.
5362
const nonce = new Uint8Array(12)
5463
new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength).setUint32(n, 4, true)
5564

5665
return nonce
5766
}
5867

59-
protected encrypt (k: bytes32, n: uint32, ad: Uint8Array, plaintext: Uint8Array): bytes {
68+
protected encrypt (k: bytes32, n: uint64, ad: Uint8Array, plaintext: Uint8Array): bytes {
69+
if (n > MAX_NONCE) {
70+
throw new Error(ERR_MAX_NONCE)
71+
}
6072
const nonce = this.nonceToBytes(n)
6173
const ctx = new ChaCha20Poly1305(k)
6274
return ctx.seal(nonce, plaintext, ad)
@@ -74,7 +86,10 @@ export abstract class AbstractHandshake {
7486
return ciphertext
7587
}
7688

77-
protected decrypt (k: bytes32, n: uint32, ad: bytes, ciphertext: bytes): {plaintext: bytes, valid: boolean} {
89+
protected decrypt (k: bytes32, n: uint64, ad: bytes, ciphertext: bytes): {plaintext: bytes, valid: boolean} {
90+
if (n > MAX_NONCE) {
91+
throw new Error(ERR_MAX_NONCE)
92+
}
7893
const nonce = this.nonceToBytes(n)
7994
const ctx = new ChaCha20Poly1305(k)
8095
const encryptedMessage = ctx.open(

0 commit comments

Comments
 (0)