Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: safely handle nonces as 64 bit uints #118

Merged
merged 1 commit into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/@types/basic.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ export type bytes = Uint8Array
export type bytes32 = Uint8Array
export type bytes16 = Uint8Array

export type uint32 = number
export type uint64 = number
6 changes: 4 additions & 2 deletions src/@types/handshake.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { bytes, bytes32, uint32, uint64 } from './basic'
import { bytes, bytes32, uint64 } from './basic'
import { KeyPair } from './libp2p'

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

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

export interface SymmetricState {
Expand Down
27 changes: 21 additions & 6 deletions src/handshakes/abstract-handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,20 @@ import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
import { fromString as uint8ArrayFromString } from 'uint8arrays'

import { bytes, bytes32, uint32 } from '../@types/basic'
import { bytes, bytes32, uint64 } from '../@types/basic'
import { CipherState, MessageBuffer, SymmetricState } from '../@types/handshake'
import { getHkdf } from '../utils'
import { logger } from '../logger'

export const MIN_NONCE = 0
// For performance reasons, the nonce is represented as a JS `number`
// JS `number` can only safely represent integers up to 2 ** 53 - 1
// This is a slight deviation from the noise spec, which describes the max nonce as 2 ** 64 - 2
// 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.
// 2 ** 53 - 1 is still a large number of messages, so the practical effect of this is negligible.
export const MAX_NONCE = Number.MAX_SAFE_INTEGER

const ERR_MAX_NONCE = 'Cipherstate has reached maximum n, a new handshake must be performed'

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

protected setNonce (cs: CipherState, nonce: uint32): void {
protected setNonce (cs: CipherState, nonce: uint64): void {
cs.n = nonce
}

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

protected incrementNonce (n: uint32): uint32 {
protected incrementNonce (n: uint64): uint64 {
return n + 1
}

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

return nonce
}

protected encrypt (k: bytes32, n: uint32, ad: Uint8Array, plaintext: Uint8Array): bytes {
protected encrypt (k: bytes32, n: uint64, ad: Uint8Array, plaintext: Uint8Array): bytes {
if (n > MAX_NONCE) {
throw new Error(ERR_MAX_NONCE)
}
const nonce = this.nonceToBytes(n)
const ctx = new ChaCha20Poly1305(k)
return ctx.seal(nonce, plaintext, ad)
Expand All @@ -74,7 +86,10 @@ export abstract class AbstractHandshake {
return ciphertext
}

protected decrypt (k: bytes32, n: uint32, ad: bytes, ciphertext: bytes): {plaintext: bytes, valid: boolean} {
protected decrypt (k: bytes32, n: uint64, ad: bytes, ciphertext: bytes): {plaintext: bytes, valid: boolean} {
if (n > MAX_NONCE) {
throw new Error(ERR_MAX_NONCE)
}
const nonce = this.nonceToBytes(n)
const ctx = new ChaCha20Poly1305(k)
const encryptedMessage = ctx.open(
Expand Down