Skip to content

Commit a07d991

Browse files
authored
chore: merge nonce and decryption fixes (#47)
2 parents d56d0c8 + 3b48677 commit a07d991

File tree

5 files changed

+70
-10
lines changed

5 files changed

+70
-10
lines changed

build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ repositories {
2222
dependencies {
2323
testImplementation(kotlin("test"))
2424
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
25-
testImplementation("org.bouncycastle:bcprov-jdk15on:1.70")
26-
testImplementation("org.bouncycastle:bcpkix-jdk15on:1.70")
25+
testImplementation("org.bouncycastle:bcprov-jdk18on:1.77")
26+
testImplementation("org.bouncycastle:bcpkix-jdk18on:1.77")
2727
}
2828

2929
java {

src/main/kotlin/Cipher.kt

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@ package nl.sanderdijkhuis.noise
33
import nl.sanderdijkhuis.noise.cryptography.*
44
import nl.sanderdijkhuis.noise.data.State
55

6-
/** Encompasses all Noise protocol cipher state required to encrypt and decrypt data. */
6+
/**
7+
* Encompasses all Noise protocol cipher state required to encrypt and decrypt data.
8+
*
9+
* Note that as per Noise revision 34 § 5.1, [[key]] may be uninitialized. In this case [[encrypt]] and [[decrypt]]
10+
* are identity functions over the plaintext and ciphertext.
11+
*
12+
* Encryption and decryption throw if incrementing [[nonce]] results in its maximum value: it means too many messages
13+
* have been exchanged. Too many is a lot indeed: 2^64-1.
14+
*/
715
data class Cipher(val cryptography: Cryptography, val key: CipherKey? = null, val nonce: Nonce = Nonce.zero) {
816

917
fun encrypt(associatedData: AssociatedData, plaintext: Plaintext): State<Cipher, Ciphertext> =
1018
key?.let { k ->
11-
nonce.increment()?.let {
12-
State(copy(nonce = it), cryptography.encrypt(k, nonce, associatedData, plaintext))
19+
nonce.increment().let { n ->
20+
checkNotNull(n) { "Too many messages" }
21+
State(copy(nonce = n), cryptography.encrypt(k, nonce, associatedData, plaintext))
1322
}
1423
} ?: State(this, Ciphertext(plaintext.data))
1524

1625
fun decrypt(associatedData: AssociatedData, ciphertext: Ciphertext): State<Cipher, Plaintext>? =
17-
nonce.increment()?.let { n ->
18-
key?.let {
19-
cryptography.decrypt(it, nonce, associatedData, ciphertext)?.let { p -> State(copy(nonce = n), p) }
20-
} ?: State(this, ciphertext.plaintext)
26+
nonce.increment().let { n ->
27+
checkNotNull(n) { "Too many messages" }
28+
if (key == null) return State(this, ciphertext.plaintext)
29+
cryptography.decrypt(key, nonce, associatedData, ciphertext)?.let { p -> State(copy(nonce = n), p) }
2130
}
2231
}

src/main/kotlin/cryptography/Nonce.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ value class Nonce(val value: ULong) {
99

1010
val bytes: ByteArray get() = SIZE.byteArray { (value shr (it * Byte.SIZE_BITS)).toByte() }
1111

12-
fun increment(): Nonce? = if (value == ULong.MAX_VALUE) null else Nonce(value + 1uL)
12+
fun increment(): Nonce? = if (value >= ULong.MAX_VALUE - 1uL) null else Nonce(value + 1uL)
1313

1414
companion object {
1515

src/test/kotlin/CipherTest.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package nl.sanderdijkhuis.noise
2+
3+
import nl.sanderdijkhuis.noise.cryptography.AssociatedData
4+
import nl.sanderdijkhuis.noise.cryptography.CipherKey
5+
import nl.sanderdijkhuis.noise.cryptography.Nonce
6+
import nl.sanderdijkhuis.noise.cryptography.Plaintext
7+
import nl.sanderdijkhuis.noise.data.Data
8+
import org.junit.jupiter.api.assertThrows
9+
import kotlin.test.Test
10+
import kotlin.test.assertNull
11+
12+
@OptIn(ExperimentalStdlibApi::class)
13+
class CipherTest {
14+
private val data = AssociatedData(Data.empty)
15+
private val otherData = AssociatedData(Data("other".toByteArray()))
16+
private val plaintext = Plaintext(Data.empty)
17+
18+
@Test
19+
fun `throws upon reaching nonce maximum while encrypting`() {
20+
val nonceTooHighToToUse = Nonce(ULong.MAX_VALUE - 1uL) // 2^64-2
21+
22+
assertThrows<IllegalStateException> { cipher(nonceTooHighToToUse).encrypt(data, plaintext) }
23+
}
24+
25+
@Test
26+
fun `throws upon reaching nonce maximum while decrypting`() {
27+
val nonceTooHighToEncrypt = Nonce(ULong.MAX_VALUE - 2uL) // 2^64-3
28+
val (cipher, ciphertext) = cipher(nonceTooHighToEncrypt).encrypt(data, plaintext)
29+
30+
assertThrows<IllegalStateException> { cipher.decrypt(data, ciphertext) }
31+
}
32+
33+
@Test
34+
fun `signals an error to the caller upon authentication failure during decryption`() {
35+
val (cipher, ciphertext) = cipher(Nonce.zero).encrypt(data, plaintext)
36+
37+
assertNull(cipher.decrypt(otherData, ciphertext))
38+
}
39+
40+
private fun cipher(nonce: Nonce) =
41+
Cipher(
42+
JavaCryptography,
43+
CipherKey(Data("76fef1ab184aa7539e3b62a43019ecafc621248b3ac2f5297dd5814e3bd560d3".hexToByteArray())),
44+
nonce
45+
)
46+
}

src/test/kotlin/NonceTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class NonceTest {
2424
assertNull(Nonce(ULong.MAX_VALUE).increment())
2525
}
2626

27+
@Test
28+
fun `never increment to 2^64-1 which is reserved for other use`() {
29+
assertNull(Nonce(ULong.MAX_VALUE - 1uL).increment())
30+
}
31+
2732
@Test
2833
fun testEncodeLittleEndian() {
2934
assertEquals((0uL).toLong(), 0L)

0 commit comments

Comments
 (0)