Skip to content

Commit 5f945a0

Browse files
committed
fix: safely handle nonces as 64 bit uints
1 parent 61251ed commit 5f945a0

3 files changed

Lines changed: 25 additions & 9 deletions

File tree

src/@types/basic.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,4 @@ export type bytes = Buffer
44
export type bytes32 = Buffer
55
export type bytes16 = Buffer
66

7-
export type uint32 = number
87
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
@@ -3,12 +3,20 @@ import * as x25519 from '@stablelib/x25519'
33
import * as SHA256 from '@stablelib/sha256'
44
import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305'
55

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

1111
export const MIN_NONCE = 0
12+
// For performance reasons, the nonce is represented as a JS `number`
13+
// JS `number` can only safely represent integers up to 2 ** 53 - 1
14+
// This is a slight deviation from the noise spec, which describes the max nonce as 2 ** 64 - 2
15+
// 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.
16+
// 2 ** 53 - 1 is still a large number of messages, so the practical effect of this is negligible.
17+
export const MAX_NONCE = Number.MAX_SAFE_INTEGER
18+
19+
const ERR_MAX_NONCE = 'Cipherstate has reached maximum n, a new handshake must be performed'
1220

1321
export abstract class AbstractHandshake {
1422
public encryptWithAd (cs: CipherState, ad: bytes, plaintext: bytes): bytes {
@@ -30,7 +38,7 @@ export abstract class AbstractHandshake {
3038
return !this.isEmptyKey(cs.k)
3139
}
3240

33-
protected setNonce (cs: CipherState, nonce: uint32): void {
41+
protected setNonce (cs: CipherState, nonce: uint64): void {
3442
cs.n = nonce
3543
}
3644

@@ -43,18 +51,22 @@ export abstract class AbstractHandshake {
4351
return emptyKey.equals(k)
4452
}
4553

46-
protected incrementNonce (n: uint32): uint32 {
54+
protected incrementNonce (n: uint64): uint64 {
4755
return n + 1
4856
}
4957

50-
protected nonceToBytes (n: uint32): bytes {
58+
protected nonceToBytes (n: uint64): bytes {
59+
// Even though we're treating the nonce as 8 bytes, RFC7539 specifies 12 bytes for a nonce.
5160
const nonce = Buffer.alloc(12)
5261
nonce.writeUInt32LE(n, 4)
5362

5463
return nonce
5564
}
5665

57-
protected encrypt (k: bytes32, n: uint32, ad: bytes, plaintext: bytes): bytes {
66+
protected encrypt (k: bytes32, n: uint64, ad: bytes, plaintext: bytes): bytes {
67+
if (n > MAX_NONCE) {
68+
throw new Error(ERR_MAX_NONCE)
69+
}
5870
const nonce = this.nonceToBytes(n)
5971
const ctx = new ChaCha20Poly1305(k)
6072
const encryptedMessage = ctx.seal(nonce, plaintext, ad)
@@ -73,7 +85,10 @@ export abstract class AbstractHandshake {
7385
return ciphertext
7486
}
7587

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

0 commit comments

Comments
 (0)