2022-11-21 18:07:21 -04:00
import { HMACDRBG } from "@stablelib/hmac-drbg" ;
import { randomBytes } from "@stablelib/random" ;
2023-11-20 16:40:40 -04:00
import { SHA256 } from "@stablelib/sha256" ;
2022-11-21 18:07:21 -04:00
import { expect } from "chai" ;
import { equals as uint8ArrayEquals } from "uint8arrays/equals" ;
2022-11-22 10:13:57 -04:00
import {
NoiseHandshakeDecoder ,
NoiseHandshakeEncoder ,
NoiseSecureTransferDecoder ,
NoiseSecureTransferEncoder ,
2023-11-21 12:36:23 -04:00
} from "./codec.js" ;
import { commitPublicKey } from "./crypto.js" ;
import { DH25519 } from "./dh25519.js" ;
import { Handshake } from "./handshake.js" ;
import { MessageNametagBufferSize , MessageNametagLength } from "./messagenametag.js" ;
import { NoiseHandshakePatterns } from "./patterns.js" ;
import { NoisePublicKey } from "./publickey.js" ;
import { QR } from "./qr.js" ;
2022-11-21 18:07:21 -04:00
2023-04-04 01:34:13 +02:00
const PUBSUB_TOPIC = "default" ;
2022-11-21 18:07:21 -04:00
describe ( "Waku Noise Sessions" , ( ) = > {
const rng = new HMACDRBG ( ) ;
// This test implements the Device pairing and Secure Transfers with Noise
// detailed in the 43/WAKU2-DEVICE-PAIRING RFC https://rfc.vac.dev/spec/43/
it ( "Noise Waku Pairing Handhshake and Secure transfer" , async function ( ) {
// Pairing Phase
// ==========
2023-11-20 15:56:52 -04:00
const dhKey = new DH25519 ( ) ;
2023-11-20 16:40:40 -04:00
const hash = SHA256 ;
2023-11-20 15:56:52 -04:00
2023-11-20 15:14:02 -04:00
const hsPattern = NoiseHandshakePatterns . Noise_WakuPairing_25519_ChaChaPoly_SHA256 ;
2022-11-21 18:07:21 -04:00
// Alice static/ephemeral key initialization and commitment
2023-11-20 15:56:52 -04:00
const aliceStaticKey = dhKey . generateKeyPair ( ) ;
const aliceEphemeralKey = dhKey . generateKeyPair ( ) ;
2022-11-21 18:07:21 -04:00
const s = randomBytes ( 32 , rng ) ;
2023-11-20 16:40:40 -04:00
const aliceCommittedStaticKey = commitPublicKey ( hash , aliceStaticKey . publicKey , s ) ;
2022-11-21 18:07:21 -04:00
// Bob static/ephemeral key initialization and commitment
2023-11-20 15:56:52 -04:00
const bobStaticKey = dhKey . generateKeyPair ( ) ;
const bobEphemeralKey = dhKey . generateKeyPair ( ) ;
2022-11-21 18:07:21 -04:00
const r = randomBytes ( 32 , rng ) ;
2023-11-20 16:40:40 -04:00
const bobCommittedStaticKey = commitPublicKey ( hash , bobStaticKey . publicKey , r ) ;
2022-11-21 18:07:21 -04:00
// Content topic information
const applicationName = "waku-noise-sessions" ;
const applicationVersion = "0.1" ;
const shardId = "10" ;
const qrMessageNameTag = randomBytes ( MessageNametagLength , rng ) ;
// Out-of-band Communication
// Bob prepares the QR and sends it out-of-band to Alice
2022-12-03 09:37:39 -04:00
const qr = new QR ( applicationName , applicationVersion , shardId , bobEphemeralKey . publicKey , bobCommittedStaticKey ) ;
2022-11-21 18:07:21 -04:00
// Alice deserializes the QR code
2023-01-06 13:34:32 -04:00
const readQR = QR . from ( qr . toString ( ) ) ;
2022-11-21 18:07:21 -04:00
// We check if QR serialization/deserialization works
expect ( readQR . applicationName ) . to . be . equals ( applicationName ) ;
expect ( readQR . applicationVersion ) . to . be . equals ( applicationVersion ) ;
expect ( readQR . shardId ) . to . be . equals ( shardId ) ;
expect ( uint8ArrayEquals ( bobEphemeralKey . publicKey , readQR . ephemeralKey ) ) . to . be . true ;
expect ( uint8ArrayEquals ( bobCommittedStaticKey , readQR . committedStaticKey ) ) . to . be . true ;
// We set the contentTopic from the content topic parameters exchanged in the QR
const contentTopic =
2024-04-16 02:07:28 +02:00
"/" + applicationName + "/" + applicationVersion + "/" + shardId + "/proto" ;
2022-11-21 18:07:21 -04:00
// Pre-handshake message
// <- eB {H(sB||r), contentTopicParams, messageNametag}
const preMessagePKs = [ NoisePublicKey . fromPublicKey ( bobEphemeralKey . publicKey ) ] ;
// We initialize the Handshake states.
// Note that we pass the whole qr serialization as prologue information
const aliceHS = new Handshake ( {
hsPattern ,
ephemeralKey : aliceEphemeralKey ,
staticKey : aliceStaticKey ,
2022-12-03 09:37:39 -04:00
prologue : qr.toByteArray ( ) ,
2022-11-21 18:07:21 -04:00
preMessagePKs ,
initiator : true ,
} ) ;
const bobHS = new Handshake ( {
hsPattern ,
ephemeralKey : bobEphemeralKey ,
staticKey : bobStaticKey ,
2022-12-03 09:37:39 -04:00
prologue : qr.toByteArray ( ) ,
2022-11-21 18:07:21 -04:00
preMessagePKs ,
} ) ;
// Pairing Handshake
// ==========
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
// 1st step
// -> eA, eAeB {H(sA||s)} [authcode]
// The messageNametag for the first handshake message is randomly generated and exchanged out-of-band
// and corresponds to qrMessageNametag
// We set the transport message to be H(sA||s)
let sentTransportMessage = aliceCommittedStaticKey ;
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
// and the (encrypted) transport message
// The message is sent with a messageNametag equal to the one received through the QR code
let aliceStep = aliceHS . stepHandshake ( {
transportMessage : sentTransportMessage ,
messageNametag : qrMessageNameTag ,
} ) ;
let encoder = new NoiseHandshakeEncoder ( contentTopic , aliceStep ) ;
// We prepare a Waku message from Alice's payload2
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
2023-04-04 01:34:13 +02:00
let wakuMsgBytes = await encoder . toWire ( { payload : new Uint8Array ( ) } ) ;
2022-11-21 18:07:21 -04:00
// We decode the WakuMessage from the ProtoBuffer
let decoder = new NoiseHandshakeDecoder ( contentTopic ) ;
2023-01-31 01:42:28 +01:00
let wakuMsgProto = await decoder . fromWireToProtoObj ( wakuMsgBytes ! ) ;
2023-04-04 01:34:13 +02:00
let v2Msg = await decoder . fromProtoObj ( PUBSUB_TOPIC , wakuMsgProto ! ) ;
2022-11-21 18:07:21 -04:00
expect ( v2Msg ! . contentTopic ) . to . be . equals ( contentTopic ) ;
expect ( v2Msg ? . payloadV2 . equals ( aliceStep . payload2 ) ) . to . be . true ;
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
// Note that Bob verifies if the received payloadv2 has the expected messageNametag set
let bobStep = bobHS . stepHandshake ( { readPayloadV2 : v2Msg?.payloadV2 , messageNametag : qrMessageNameTag } ) ;
expect ( uint8ArrayEquals ( bobStep . transportMessage , sentTransportMessage ) ) ;
// We generate an authorization code using the handshake state
const aliceAuthcode = aliceHS . genAuthcode ( ) ;
const bobAuthcode = bobHS . genAuthcode ( ) ;
// We check that they are equal. Note that this check has to be confirmed with a user interaction.
expect ( aliceAuthcode ) . to . be . equals ( bobAuthcode ) ;
// 2nd step
// <- sB, eAsB {r}
// Alice and Bob update their local next messageNametag using the available handshake information
// During the handshake, messageNametag = HKDF(h), where h is the handshake hash value at the end of the last processed message
let aliceMessageNametag = aliceHS . hs . toMessageNametag ( ) ;
let bobMessageNametag = bobHS . hs . toMessageNametag ( ) ;
// We set as a transport message the commitment randomness r
sentTransportMessage = r ;
// At this step, Bob writes and returns a payload
bobStep = bobHS . stepHandshake ( { transportMessage : sentTransportMessage , messageNametag : bobMessageNametag } ) ;
// We prepare a Waku message from Bob's payload2
encoder = new NoiseHandshakeEncoder ( contentTopic , bobStep ) ;
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
2023-04-04 01:34:13 +02:00
wakuMsgBytes = await encoder . toWire ( { payload : new Uint8Array ( ) } ) ;
2022-11-21 18:07:21 -04:00
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder ( contentTopic ) ;
2023-01-31 01:42:28 +01:00
wakuMsgProto = await decoder . fromWireToProtoObj ( wakuMsgBytes ! ) ;
2023-04-04 01:34:13 +02:00
v2Msg = await decoder . fromProtoObj ( PUBSUB_TOPIC , wakuMsgProto ! ) ;
2022-11-21 18:07:21 -04:00
expect ( v2Msg ? . payloadV2 . equals ( bobStep . payload2 ) ) . to . be . true ;
// While Alice reads and returns the (decrypted) transport message
aliceStep = aliceHS . stepHandshake ( { readPayloadV2 : v2Msg?.payloadV2 , messageNametag : aliceMessageNametag } ) ;
expect ( uint8ArrayEquals ( aliceStep . transportMessage , sentTransportMessage ) ) ;
// Alice further checks if Bob's commitment opens to Bob's static key she just received
2023-11-20 16:40:40 -04:00
const expectedBobCommittedStaticKey = commitPublicKey ( hash , aliceHS . hs . rs ! , aliceStep . transportMessage ) ;
2022-11-21 18:07:21 -04:00
expect ( uint8ArrayEquals ( expectedBobCommittedStaticKey , bobCommittedStaticKey ) ) . to . be . true ;
// 3rd step
// -> sA, sAeB, sAsB {s}
// Alice and Bob update their local next messageNametag using the available handshake information
aliceMessageNametag = aliceHS . hs . toMessageNametag ( ) ;
bobMessageNametag = bobHS . hs . toMessageNametag ( ) ;
// We set as a transport message the commitment randomness s
sentTransportMessage = s ;
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = aliceHS . stepHandshake ( { transportMessage : sentTransportMessage , messageNametag : aliceMessageNametag } ) ;
// We prepare a Waku message from Alice's payload2
encoder = new NoiseHandshakeEncoder ( contentTopic , aliceStep ) ;
// At this point wakuMsg is sent over the Waku network and is received
// We simulate this by creating the ProtoBuffer from wakuMsg
2023-04-04 01:34:13 +02:00
wakuMsgBytes = await encoder . toWire ( { payload : new Uint8Array ( ) } ) ;
2022-11-21 18:07:21 -04:00
// We decode the WakuMessage from the ProtoBuffer
decoder = new NoiseHandshakeDecoder ( contentTopic ) ;
2023-01-31 01:42:28 +01:00
wakuMsgProto = await decoder . fromWireToProtoObj ( wakuMsgBytes ! ) ;
2023-04-04 01:34:13 +02:00
v2Msg = await decoder . fromProtoObj ( PUBSUB_TOPIC , wakuMsgProto ! ) ;
2022-11-21 18:07:21 -04:00
expect ( v2Msg ? . payloadV2 . equals ( aliceStep . payload2 ) ) . to . be . true ;
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = bobHS . stepHandshake ( { readPayloadV2 : v2Msg?.payloadV2 , messageNametag : bobMessageNametag } ) ;
expect ( uint8ArrayEquals ( bobStep . transportMessage , sentTransportMessage ) ) ;
// Bob further checks if Alice's commitment opens to Alice's static key he just received
2023-11-20 16:40:40 -04:00
const expectedAliceCommittedStaticKey = commitPublicKey ( hash , bobHS . hs . rs ! , bobStep . transportMessage ) ;
2022-11-21 18:07:21 -04:00
expect ( uint8ArrayEquals ( expectedAliceCommittedStaticKey , aliceCommittedStaticKey ) ) . to . be . true ;
// Secure Transfer Phase
// ==========
2022-11-22 10:13:57 -04:00
// We finalize the handshake to retrieve the Inbound/Outbound Symmetric States
const aliceHSResult = aliceHS . finalizeHandshake ( ) ;
const bobHSResult = bobHS . finalizeHandshake ( ) ;
const aliceEncoder = new NoiseSecureTransferEncoder ( contentTopic , aliceHSResult ) ;
const bobEncoder = new NoiseSecureTransferEncoder ( contentTopic , bobHSResult ) ;
const aliceDecoder = new NoiseSecureTransferDecoder ( contentTopic , aliceHSResult ) ;
const bobDecoder = new NoiseSecureTransferDecoder ( contentTopic , bobHSResult ) ;
// We test read/write of random messages exchanged between Alice and Bob
// Note that we exchange more than the number of messages contained in the nametag buffer to test if they are filled correctly as the communication proceeds
for ( let i = 0 ; i < 10 * MessageNametagBufferSize ; i ++ ) {
// Alice writes to Bob
let message = randomBytes ( 32 , rng ) ;
2023-01-31 01:42:28 +01:00
let encodedMsg = await aliceEncoder . toWire ( { payload : message } ) ;
let readMessageProto = await bobDecoder . fromWireToProtoObj ( encodedMsg ! ) ;
2023-04-04 01:34:13 +02:00
let readMessage = await bobDecoder . fromProtoObj ( PUBSUB_TOPIC , readMessageProto ! ) ;
2022-11-22 10:13:57 -04:00
expect ( uint8ArrayEquals ( message , readMessage ! . payload ) ) . to . be . true ;
// Bob writes to Alice
message = randomBytes ( 32 , rng ) ;
2023-01-31 01:42:28 +01:00
encodedMsg = await bobEncoder . toWire ( { payload : message } ) ;
readMessageProto = await aliceDecoder . fromWireToProtoObj ( encodedMsg ! ) ;
2023-04-04 01:34:13 +02:00
readMessage = await aliceDecoder . fromProtoObj ( PUBSUB_TOPIC , readMessageProto ! ) ;
2022-11-22 10:13:57 -04:00
expect ( uint8ArrayEquals ( message , readMessage ! . payload ) ) . to . be . true ;
}
// We test how nametag buffers help in detecting lost messages
// Alice writes two messages to Bob, but only the second is received
2022-11-28 15:59:26 -04:00
let message = randomBytes ( 32 , rng ) ;
let payload2 = aliceHSResult . writeMessage ( message ) ;
message = randomBytes ( 32 , rng ) ;
payload2 = aliceHSResult . writeMessage ( message ) ;
2022-11-22 10:13:57 -04:00
try {
2022-11-28 15:59:26 -04:00
bobHSResult . readMessage ( payload2 ) ;
expect ( false , "should not reach here" ) . to . be . true ;
} catch ( err ) {
let message ;
if ( err instanceof Error ) message = err . message ;
else message = String ( err ) ;
expect ( message ) . to . be . equals ( "nametag is not ok" ) ;
2022-11-22 10:13:57 -04:00
}
// We adjust bob nametag buffer for next test (i.e. the missed message is correctly recovered)
bobHSResult . nametagsInbound . delete ( 2 ) ;
2022-11-28 15:59:26 -04:00
message = randomBytes ( 32 , rng ) ;
payload2 = bobHSResult . writeMessage ( message ) ;
const readMessage = aliceHSResult . readMessage ( payload2 ) ;
expect ( uint8ArrayEquals ( message , readMessage ) ) . to . be . true ;
2022-11-22 10:13:57 -04:00
// We test if a missing nametag is correctly detected
2022-11-28 15:59:26 -04:00
message = randomBytes ( 32 , rng ) ;
payload2 = aliceHSResult . writeMessage ( message ) ;
bobHSResult . nametagsInbound . delete ( 1 ) ;
2022-11-22 10:13:57 -04:00
try {
2022-11-28 15:59:26 -04:00
bobHSResult . readMessage ( payload2 ) ;
} catch ( err ) {
let message ;
if ( err instanceof Error ) message = err . message ;
else message = String ( err ) ;
expect ( message ) . to . be . equals ( "nametag is not ok" ) ;
2022-11-22 10:13:57 -04:00
}
2022-11-21 18:07:21 -04:00
} ) ;
} ) ;