initial commit

This commit is contained in:
Richard Ramos 2022-11-13 09:39:26 -04:00
commit b1390e0855
No known key found for this signature in database
GPG Key ID: BD36D48BC9FFC88C
32 changed files with 17557 additions and 0 deletions

26
.cspell.json Normal file
View File

@ -0,0 +1,26 @@
{
"version": "0.1",
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/master/cspell.schema.json",
"language": "en",
"words": ["Waku", "keypair", "nwaku"],
"flagWords": [],
"ignorePaths": [
"package.json",
"package-lock.json",
"yarn.lock",
"tsconfig.json",
"node_modules/**",
"build",
"gen",
"proto",
"*.spec.ts",
"src/resources.ts"
],
"patterns": [
{
"name": "import",
"pattern": "/import .*/"
}
],
"ignoreRegExpList": ["import"]
}

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false

70
.eslintrc.json Normal file
View File

@ -0,0 +1,70 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.dev.json"
},
"env": {
"es6": true
},
"ignorePatterns": ["node_modules", "build", "coverage", "proto"],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"plugin:prettier/recommended"
],
"globals": {
"BigInt": true,
"console": true,
"WebAssembly": true
},
"rules": {
"@typescript-eslint/explicit-function-return-type": [
"error",
{
"allowExpressions": true
}
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{
"allowWholeFile": true
}
],
"eslint-comments/no-unused-disable": "error",
"import/order": [
"error",
{
"newlines-between": "always",
"alphabetize": {
"order": "asc"
}
}
],
"no-constant-condition": [
"error",
{
"checkLoops": false
}
],
"sort-imports": [
"error",
{
"ignoreDeclarationSort": true,
"ignoreCase": true
}
]
},
"overrides": [
{
"files": ["*.spec.ts", "**/test_utils/*.ts"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off"
}
}
]
}

View File

@ -0,0 +1,15 @@
name: Add new issues to Waku project board
on:
issues:
types: [opened]
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@v0.3.0
with:
project-url: https://github.com/orgs/waku-org/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

46
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,46 @@
name: CI
on:
push:
branches:
- "master"
- "staging"
- "trying"
pull_request:
env:
NODE_JS: "16"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: bahmutov/npm-install@v1
- run: npm run test:lint
- run: npm run test:prettier
- run: npm run test:spelling
- run: npm run test:tsc
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: bahmutov/npm-install@v1
- run: npm run build
browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_JS }}
- uses: bahmutov/npm-install@v1
- run: npm run test:browser

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.idea/*
.angular
build
dist
node_modules
coverage
*.log
/tsconfig.tsbuildinfo
/tsconfig.dev.tsbuildinfo
/bundle/

11
.mocharc.json Normal file
View File

@ -0,0 +1,11 @@
{
"extension": ["ts"],
"spec": "src/**/*.spec.ts",
"require": ["ts-node/register", "isomorphic-fetch", "jsdom-global/register"],
"loader": "ts-node/esm",
"node-option": [
"experimental-specifier-resolution=node",
"loader=ts-node/esm"
],
"exit": true
}

7
.size-limit.cjs Normal file
View File

@ -0,0 +1,7 @@
module.exports = [
{
name: "RLN core",
path: "bundle/index.js",
import: "{ RLN }",
},
];

202
LICENSE-APACHE-v2 Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 Status Research & Development GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
LICENSE-MIT Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2021 Status Research & Development GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

9
bors.toml Normal file
View File

@ -0,0 +1,9 @@
status = [
"check",
"build",
"proto",
"browser",
"node",
]
block_labels = ["work-in-progress"]
delete_merged_branches = true

7
buf.gen.yaml Normal file
View File

@ -0,0 +1,7 @@
version: v1beta1
plugins:
- name: ts_proto
out: ./src/proto
strategy: all
opt: grpc_js,esModuleInterop=true,forceLong=long

5
buf.yaml Normal file
View File

@ -0,0 +1,5 @@
version: v1beta1
build:
roots:
- ./proto

67
karma.conf.cjs Normal file
View File

@ -0,0 +1,67 @@
process.env.CHROME_BIN = require("puppeteer").executablePath();
const os = require("os");
const path = require("path");
const ResolveTypeScriptPlugin = require("resolve-typescript-plugin");
const output = {
path:
path.join(os.tmpdir(), "_karma_webpack_") +
Math.floor(Math.random() * 1000000),
};
module.exports = function (config) {
config.set({
frameworks: ["webpack", "mocha"],
preprocessors: {
"**/*.ts": ["webpack"],
},
files: [
"src/**/*.spec.ts",
"src/**/*.ts",
{
pattern: `${output.path}/**/*`,
watched: false,
included: false,
served: true,
},
],
envPreprocessor: ["CI"],
reporters: ["progress"],
browsers: ["ChromeHeadless"],
singleRun: true,
client: {
mocha: {
timeout: 6000, // Default is 2s
},
},
webpack: {
mode: "production",
resolve: {
// Add `.ts` and `.tsx` as a resolvable extension.
extensions: [".ts", ".tsx", ".js"],
plugins: [new ResolveTypeScriptPlugin()],
},
module: {
rules: [
{
test: /\.wasm$/,
type: "asset/resource",
},
{
test: /\.(js|tsx?)$/,
loader: "ts-loader",
exclude: /node_modules|\.d\.ts$/,
options: { configFile: "tsconfig.karma.json" },
},
{
test: /\.d\.ts$/,
loader: "ignore-loader",
},
],
},
output,
},
});
};

15662
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

119
package.json Normal file
View File

@ -0,0 +1,119 @@
{
"name": "@waku/noise",
"version": "0.0.1",
"description": "",
"types": "./dist/index.d.ts",
"module": "./dist/index.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"type": "module",
"scripts": {
"prepare": "husky install",
"build": "run-s build:**",
"build:tsc": "tsc",
"build:bundle": "rollup --config rollup.config.js",
"size": "npm run build && size-limit",
"fix": "run-s fix:*",
"fix:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.*js\" \".github/**/*.yml\" --write",
"fix:lint": "eslint src --ext .ts --ext .cjs --fix",
"test": "run-s test:*",
"test:lint": "eslint src --ext .ts",
"test:prettier": "prettier \"src/**/*.ts\" \"./*.json\" \"*.*js\" \".github/**/*.yml\" --list-different",
"test:spelling": "cspell \"{*.md,.github/*.md,src/**/*.ts}\"",
"test:tsc": "tsc -p tsconfig.dev.json",
"test:browser": "karma start karma.conf.cjs",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "mocha --watch",
"prepublish": "npm run build",
"reset-hard": "git clean -dfx && git reset --hard && npm i && npm run build"
},
"browser": {
"crypto": false
},
"engines": {
"node": ">=16"
},
"author": "",
"license": "Apache-2.0 OR MIT",
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"@size-limit/preset-big-lib": "^8.0.0",
"@types/app-root-path": "^1.2.4",
"@types/chai": "^4.2.15",
"@types/debug": "^4.1.7",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
"@types/tail": "^2.0.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"cspell": "^5.14.0",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.0.2",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-prettier": "^4.0.0",
"fast-check": "^2.25.0",
"gh-pages": "^3.2.3",
"husky": "^7.0.4",
"ignore-loader": "^0.1.2",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12",
"karma-chrome-launcher": "^3.1.0",
"karma-mocha": "^2.0.1",
"karma-webpack": "^5.0.0",
"lint-staged": "^13.0.3",
"mocha": "^9.1.3",
"npm-run-all": "^4.1.5",
"p-timeout": "^4.1.0",
"prettier": "^2.1.1",
"process": "^0.11.10",
"puppeteer": "^13.0.1",
"resolve-typescript-plugin": "^1.2.0",
"rollup": "^2.75.0",
"size-limit": "^8.0.0",
"tail": "^2.2.0",
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typedoc": "^0.23.10",
"typescript": "^4.5.5"
},
"files": [
"dist",
"bundle",
"src/**.ts",
"src/**.js",
"!**/*.spec.*",
"!**/*.json",
"CHANGELOG.md",
"LICENSE",
"README.md"
],
"lint-staged": {
"*.ts": [
"eslint --fix"
],
"*.{ts,md,json,conf*.*js}": [
"prettier --write"
]
},
"dependencies": {
"@stablelib/chacha20poly1305": "^1.0.1",
"@stablelib/hkdf": "^1.0.1",
"@stablelib/sha256": "^1.0.1",
"@stablelib/x25519": "^1.0.1",
"pkcs7-padding": "^0.1.1",
"uint8arraylist": "^2.3.2",
"uint8arrays": "^4.0.2"
}
}

21
rollup.config.js Normal file
View File

@ -0,0 +1,21 @@
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
export default {
input: {
index: "dist/index.js",
},
output: {
dir: "bundle",
format: "esm",
},
plugins: [
commonjs(),
json(),
nodeResolve({
browser: true,
preferBuiltins: false,
extensions: [".js", ".ts"],
}),
],
};

5
src/@types/basic.ts Normal file
View File

@ -0,0 +1,5 @@
export type bytes = Uint8Array;
export type bytes32 = Uint8Array;
export type bytes16 = Uint8Array;
export type uint64 = number;

49
src/@types/handshake.ts Normal file
View File

@ -0,0 +1,49 @@
import type { Nonce } from "../nonce.js";
import type { bytes, bytes32, uint64 } from "./basic.js";
import type { KeyPair } from "./keypair.js";
export type Hkdf = [bytes, bytes, bytes];
export interface MessageBuffer {
ne: bytes32;
ns: bytes;
ciphertext: bytes;
}
export interface CipherState {
k: bytes32;
// For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce;
}
export interface SymmetricState {
cs: CipherState;
ck: bytes32; // chaining key
h: bytes32; // handshake hash
}
export interface HandshakeState {
ss: SymmetricState;
s: KeyPair;
e?: KeyPair;
rs: bytes32;
re: bytes32;
psk: bytes32;
}
export interface NoiseSession {
hs: HandshakeState;
h?: bytes32;
cs1?: CipherState;
cs2?: CipherState;
mc: uint64;
i: boolean;
}
export interface INoisePayload {
identityKey: bytes;
identitySig: bytes;
data: bytes;
}

6
src/@types/keypair.ts Normal file
View File

@ -0,0 +1,6 @@
import type { bytes32 } from "./basic.js";
export interface KeyPair {
publicKey: bytes32;
privateKey: bytes32;
}

6
src/@types/pkcs7-padding/index.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module "pkcs7-padding" {
export function pad(data: Uint8Array, size?: number): Uint8Array;
export function pad(data: string, size?: number): string;
export function unpad(data: Uint8Array): Uint8Array;
export function unpad(data: string): string;
}

96
src/crypto.ts Normal file
View File

@ -0,0 +1,96 @@
import { ChaCha20Poly1305 } from "@stablelib/chacha20poly1305";
import { HKDF } from "@stablelib/hkdf";
import { hash, SHA256 } from "@stablelib/sha256";
import * as x25519 from "@stablelib/x25519";
import type { bytes, bytes32 } from "./@types/basic.js";
import type { Hkdf } from "./@types/handshake.js";
import type { KeyPair } from "./@types/keypair.js";
export const Curve25519KeySize = x25519.PUBLIC_KEY_LENGTH;
export function hashSHA256(data: Uint8Array): Uint8Array {
return hash(data);
}
export function intoCurve25519Key(s: Uint8Array): bytes32 {
if (s.length != x25519.PUBLIC_KEY_LENGTH) {
throw "invalid public key length";
}
return s;
}
export function getHKDF(ck: bytes32, ikm: Uint8Array): Hkdf {
const hkdf = new HKDF(SHA256, ikm, ck);
const okmU8Array = hkdf.expand(96);
const okm = okmU8Array;
const k1 = okm.subarray(0, 32);
const k2 = okm.subarray(32, 64);
const k3 = okm.subarray(64, 96);
return [k1, k2, k3];
}
export function generateX25519KeyPair(): KeyPair {
const keypair = x25519.generateKeyPair();
return {
publicKey: keypair.publicKey,
privateKey: keypair.secretKey,
};
}
export function generateX25519KeyPairFromSeed(seed: Uint8Array): KeyPair {
const keypair = x25519.generateKeyPairFromSeed(seed);
return {
publicKey: keypair.publicKey,
privateKey: keypair.secretKey,
};
}
export function generateX25519SharedKey(
privateKey: Uint8Array,
publicKey: Uint8Array
): Uint8Array {
return x25519.sharedKey(privateKey, publicKey);
}
export function chaCha20Poly1305Encrypt(
plaintext: Uint8Array,
nonce: Uint8Array,
ad: Uint8Array,
k: bytes32
): bytes {
const ctx = new ChaCha20Poly1305(k);
return ctx.seal(nonce, plaintext, ad);
}
export function chaCha20Poly1305Decrypt(
ciphertext: Uint8Array,
nonce: Uint8Array,
ad: Uint8Array,
k: bytes32
): bytes | null {
const ctx = new ChaCha20Poly1305(k);
return ctx.open(nonce, ciphertext, ad);
}
export function dh(privateKey: bytes32, publicKey: bytes32): bytes32 {
try {
const derivedU8 = generateX25519SharedKey(privateKey, publicKey);
if (derivedU8.length === 32) {
return derivedU8;
}
return derivedU8.subarray(0, 32);
} catch (e) {
console.error(e);
return new Uint8Array(32);
}
}

501
src/handshake.ts Normal file
View File

@ -0,0 +1,501 @@
import * as pkcs7 from "pkcs7-padding";
import { bytes32 } from "./@types/basic.js";
import type { KeyPair } from "./@types/keypair.js";
import {
Curve25519KeySize,
dh,
generateX25519KeyPair,
intoCurve25519Key,
} from "./crypto.js";
import { SymmetricState } from "./noise.js";
import {
EmptyPreMessage,
HandshakePattern,
MessageDirection,
NoiseTokens,
PreMessagePattern,
} from "./patterns";
import { NoisePublicKey } from "./publickey.js";
// The padding blocksize of a transport message
const NoisePaddingBlockSize = 248;
// The Handshake State as in https://noiseprotocol.org/noise.html#the-handshakestate-object
// Contains
// - the local and remote ephemeral/static keys e,s,re,rs (if any)
// - the initiator flag (true if the user creating the state is the handshake initiator, false otherwise)
// - the handshakePattern (containing the handshake protocol name, and (pre)message patterns)
// This object is futher extended from specifications by storing:
// - a message pattern index msgPatternIdx indicating the next handshake message pattern to process
// - the user's preshared psk, if any
export class HandshakeState {
s?: KeyPair;
e?: KeyPair;
rs?: bytes32;
re?: bytes32;
ss: SymmetricState;
initiator: boolean;
handshakePattern: HandshakePattern;
msgPatternIdx: number;
psk: Uint8Array;
constructor(hsPattern: HandshakePattern, psk: Uint8Array) {
// By default the Handshake State initiator flag is set to false
// Will be set to true when the user associated to the handshake state starts an handshake
this.initiator = false;
this.handshakePattern = hsPattern;
this.psk = psk;
this.ss = new SymmetricState(hsPattern);
this.msgPatternIdx = 0;
}
// Handshake Processing
// Based on the message handshake direction and if the user is or not the initiator, returns a boolean tuple telling if the user
// has to read or write the next handshake message
getReadingWritingState(direction: MessageDirection): {
reading: boolean;
writing: boolean;
} {
let reading = false;
let writing = false;
if (this.initiator && direction == MessageDirection.r) {
// I'm Alice and direction is ->
reading = false;
writing = true;
} else if (this.initiator && direction == MessageDirection.l) {
// I'm Alice and direction is <-
reading = true;
writing = false;
} else if (!this.initiator && direction == MessageDirection.r) {
// I'm Bob and direction is ->
reading = true;
writing = false;
} else if (!this.initiator && direction == MessageDirection.l) {
// I'm Bob and direction is <-
reading = false;
writing = true;
}
return { reading, writing };
}
// Checks if a pre-message is valid according to Noise specifications
// http://www.noiseprotocol.org/noise.html#handshake-patterns
isValid(msg: Array<PreMessagePattern>): boolean {
let isValid = true;
// Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction
const allowedPatterns = [
new PreMessagePattern(MessageDirection.r, [NoiseTokens.s]),
new PreMessagePattern(MessageDirection.r, [NoiseTokens.e]),
new PreMessagePattern(MessageDirection.r, [NoiseTokens.e, NoiseTokens.s]),
new PreMessagePattern(MessageDirection.l, [NoiseTokens.s]),
new PreMessagePattern(MessageDirection.l, [NoiseTokens.e]),
new PreMessagePattern(MessageDirection.l, [NoiseTokens.e, NoiseTokens.s]),
];
// We check if pre message patterns are allowed
for (const pattern of msg) {
if (!allowedPatterns.find((x) => x.equals(pattern))) {
isValid = false;
break;
}
}
return isValid;
}
// Handshake messages processing procedures
// Processes pre-message patterns
processPreMessagePatternTokens(
inPreMessagePKs: Array<NoisePublicKey> = []
): void {
// I make a copy of the input pre-message public keys, so that I can easily delete processed ones without using iterators/counters
const preMessagePKs = inPreMessagePKs;
// Here we store currently processed pre message public key
let currPK: NoisePublicKey;
// We retrieve the pre-message patterns to process, if any
// If none, there's nothing to do
if (this.handshakePattern.preMessagePatterns == EmptyPreMessage) {
return;
}
// If not empty, we check that pre-message is valid according to Noise specifications
if (!this.isValid(this.handshakePattern.preMessagePatterns)) {
throw "invalid pre-message in handshake";
}
// We iterate over each pattern contained in the pre-message
for (const messagePattern of this.handshakePattern.preMessagePatterns) {
const direction = messagePattern.direction;
const tokens = messagePattern.tokens;
// We get if the user is reading or writing the current pre-message pattern
const { reading, writing } = this.getReadingWritingState(direction);
// We process each message pattern token
for (const token of tokens) {
// We process the pattern token
switch (token) {
case NoiseTokens.e:
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0];
} else {
throw "noise pre-message read e, expected a public key";
}
// If user is reading the "e" token
if (reading) {
console.trace("noise pre-message read e");
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
if (currPK.flag == 0) {
// Sets re and calls MixHash(re.public_key).
this.re = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.re);
} else {
throw "noise read e, incorrect encryption flag for pre-message public key";
}
// If user is writing the "e" token
} else if (writing) {
console.trace("noise pre-message write e");
// When writing, the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(e.public_key).
if (this.e && this.e.publicKey == intoCurve25519Key(currPK.pk)) {
this.ss.mixHash(this.e.publicKey);
} else {
throw "noise pre-message e key doesn't correspond to locally set e key pair";
}
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if (this.handshakePattern.name.indexOf("psk") > -1) {
this.ss.mixKey(currPK.pk);
}
// We delete processed public key
preMessagePKs.shift();
break;
case NoiseTokens.s:
// We expect a static key, so we attempt to read it (next PK to process will always be at index of preMessagePKs)
if (preMessagePKs.length > 0) {
currPK = preMessagePKs[0];
} else {
throw "noise pre-message read s, expected a public key";
}
// If user is reading the "s" token
if (reading) {
console.trace("noise pre-message read s");
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
if (currPK.flag == 0) {
// Sets re and calls MixHash(re.public_key).
this.rs = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.rs);
} else {
throw "noise read s, incorrect encryption flag for pre-message public key";
}
// If user is writing the "s" token
} else if (writing) {
console.trace("noise pre-message write s");
// If writing, it means that the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(s.public_key).
if (this.s && this.s.publicKey == intoCurve25519Key(currPK.pk)) {
this.ss.mixHash(this.s.publicKey);
} else {
throw "noise pre-message s key doesn't correspond to locally set s key pair";
}
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if (this.handshakePattern.name.indexOf("psk") > -1) {
this.ss.mixKey(currPK.pk);
}
// We delete processed public key
preMessagePKs.shift();
break;
default:
throw "invalid Token for pre-message pattern";
}
}
}
}
// This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
// An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
processMessagePatternPayload(
transportMessage: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
let payload: Uint8Array;
// We retrieve current message pattern (direction + tokens) to process
const direction =
this.handshakePattern.messagePatterns[this.msgPatternIdx].direction;
// We get if the user is reading or writing the input handshake message
const { reading, writing } = this.getReadingWritingState(direction);
// We decrypt the transportMessage, if any
if (reading) {
payload = this.ss.decryptAndHash(transportMessage, extraAd);
payload = pkcs7.pad(payload, NoisePaddingBlockSize);
} else if (writing) {
payload = pkcs7.unpad(transportMessage);
payload = this.ss.encryptAndHash(payload, extraAd);
} else {
throw "undefined state";
}
return payload;
}
// We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
processMessagePatternTokens(
inputHandshakeMessage: Array<NoisePublicKey> = []
): Array<NoisePublicKey> {
// We retrieve current message pattern (direction + tokens) to process
const messagePattern =
this.handshakePattern.messagePatterns[this.msgPatternIdx];
const direction = messagePattern.direction;
const tokens = messagePattern.tokens;
// We get if the user is reading or writing the input handshake message
const { reading, writing } = this.getReadingWritingState(direction);
// I make a copy of the handshake message so that I can easily delete processed PKs without using iterators/counters
// (Possibly) non-empty if reading
const inHandshakeMessage = inputHandshakeMessage;
// The party's output public keys
// (Possibly) non-empty if writing
const outHandshakeMessage: Array<NoisePublicKey> = [];
// In currPK we store the currently processed public key from the handshake message
let currPK: NoisePublicKey;
// We process each message pattern token
for (const token of tokens) {
switch (token) {
case NoiseTokens.e:
// If user is reading the "s" token
if (reading) {
console.trace("noise read e");
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0];
} else {
throw "noise read e, expected a public key";
}
// We check if current key is encrypted or not
// Note: by specification, ephemeral keys should always be unencrypted. But we support encrypted ones.
if (currPK.flag == 0) {
// Unencrypted Public Key
// Sets re and calls MixHash(re.public_key).
this.re = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.re);
// The following is out of specification: we call decryptAndHash for encrypted ephemeral keys, similarly as happens for (encrypted) static keys
} else if (currPK.flag == 1) {
// Encrypted public key
// Decrypts re, sets re and calls MixHash(re.public_key).
this.re = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else {
throw "noise read e, incorrect encryption flag for public key";
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if (this.handshakePattern.name.indexOf("psk") > -1) {
this.ss.mixKey(this.re);
}
// We delete processed public key
inHandshakeMessage.shift();
// If user is writing the "e" token
} else if (writing) {
console.trace("noise write e");
// We generate a new ephemeral keypair
this.e = generateX25519KeyPair();
// We update the state
this.ss.mixHash(this.e.publicKey);
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if (this.handshakePattern.name.indexOf("psk") > -1) {
this.ss.mixKey(this.e.publicKey);
}
// We add the ephemeral public key to the Waku payload
outHandshakeMessage.push(NoisePublicKey.to(this.e.publicKey));
}
break;
case NoiseTokens.s:
// If user is reading the "s" token
if (reading) {
console.trace("noise read s");
// We expect a static key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if (inHandshakeMessage.length > 0) {
currPK = inHandshakeMessage[0];
} else {
throw "noise read s, expected a public key";
}
// We check if current key is encrypted or not
if (currPK.flag == 0) {
// Unencrypted Public Key
// Sets re and calls MixHash(re.public_key).
this.rs = intoCurve25519Key(currPK.pk);
this.ss.mixHash(this.rs);
} else if (currPK.flag == 1) {
// Encrypted public key
// Decrypts rs, sets rs and calls MixHash(rs.public_key).
this.rs = intoCurve25519Key(this.ss.decryptAndHash(currPK.pk));
} else {
throw "noise read s, incorrect encryption flag for public key";
}
// We delete processed public key
inHandshakeMessage.shift();
// If user is writing the "s" token
} else if (writing) {
console.trace("noise write s");
// If the local static key is not set (the handshake state was not properly initialized), we raise an error
if (!this.s) {
throw "static key not set";
}
// We encrypt the public part of the static key in case a key is set in the Cipher State
// That is, encS may either be an encrypted or unencrypted static key.
const encS = this.ss.encryptAndHash(this.s.publicKey);
// We add the (encrypted) static public key to the Waku payload
// Note that encS = (Enc(s) || tag) if encryption key is set, otherwise encS = s.
// We distinguish these two cases by checking length of encryption and we set the proper encryption flag
if (encS.length > Curve25519KeySize) {
outHandshakeMessage.push(new NoisePublicKey(1, encS));
} else {
outHandshakeMessage.push(new NoisePublicKey(0, encS));
}
}
break;
case NoiseTokens.psk:
// If user is reading the "psk" token
console.trace("noise psk");
// Calls MixKeyAndHash(psk)
this.ss.mixKeyAndHash(this.psk);
break;
case NoiseTokens.ee:
// If user is reading the "ee" token
console.trace("noise dh ee");
// If local and/or remote ephemeral keys are not set, we raise an error
if (!this.e || !this.re) {
throw "local or remote ephemeral key not set";
}
// Calls MixKey(DH(e, re)).
this.ss.mixKey(dh(this.e.privateKey, this.re));
break;
case NoiseTokens.es:
// If user is reading the "es" token
console.trace("noise dh es");
// We check if keys are correctly set.
// If both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
if (this.initiator) {
if (!this.e || !this.rs) {
throw "local or remote ephemeral/static key not set";
}
this.ss.mixKey(dh(this.e.privateKey, this.rs));
} else {
if (!this.re || !this.s) {
throw "local or remote ephemeral/static key not set";
}
this.ss.mixKey(dh(this.s.privateKey, this.re));
}
break;
case NoiseTokens.se:
// If user is reading the "se" token
console.trace("noise dh se");
// We check if keys are correctly set.
// If both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
if (this.initiator) {
if (!this.s || !this.re) {
throw "local or remote ephemeral/static key not set";
}
this.ss.mixKey(dh(this.s.privateKey, this.re));
} else {
if (!this.rs || !this.e) {
throw "local or remote ephemeral/static key not set";
}
this.ss.mixKey(dh(this.e.privateKey, this.rs));
}
break;
case NoiseTokens.ss:
// If user is reading the "ss" token
console.trace("noise dh ss");
// If local and/or remote static keys are not set, we raise an error
if (!this.s || !this.rs) {
throw "local or remote static key not set";
}
// Calls MixKey(DH(s, rs)).
this.ss.mixKey(dh(this.s.privateKey, this.rs));
break;
}
}
return outHandshakeMessage;
}
}

0
src/index.ts Normal file
View File

270
src/noise.ts Normal file
View File

@ -0,0 +1,270 @@
import { fromString as uint8ArrayFromString } from "uint8arrays";
import { concat as uint8ArrayConcat } from "uint8arrays/concat";
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import type { bytes32 } from "./@types/basic.js";
import {
chaCha20Poly1305Decrypt,
chaCha20Poly1305Encrypt,
getHKDF,
hashSHA256,
} from "./crypto.js";
import { Nonce } from "./nonce.js";
import { HandshakePattern } from "./patterns.js";
// Waku Noise Protocols for Waku Payload Encryption
// Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
// See spec for more details:
// https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
//
// Implementation partially inspired by noise-libp2p and js-libp2p-noise
// https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
// https://github.com/ChainSafe/js-libp2p-noise
/*
# Noise state machine primitives
# Overview :
# - Alice and Bob process (i.e. read and write, based on their role) each token appearing in a handshake pattern, consisting of pre-message and message patterns;
# - Both users initialize and update according to processed tokens a Handshake State, a Symmetric State and a Cipher State;
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
# - When a static public key s or a payload message is writted, it is encrypted with encryptAndHash;
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
# - If all tokens are processed, users compute two new Cipher States by calling Split;
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
#################################
# Cipher State Primitives
#################################
*/
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
// Contains an encryption key k and a nonce n (used in Noise as a counter)
export class CipherState {
k: bytes32;
// For performance reasons, the nonce is represented as a Nonce object
// The nonce is treated as a uint64, even though the underlying `number` only has 52 safely-available bits.
n: Nonce;
constructor(k: bytes32 = CipherState.createEmptyKey()) {
this.k = k;
this.n = new Nonce();
}
// Checks if a Cipher State has an encryption key set
protected hasKey(): boolean {
return !this.isEmptyKey(this.k);
}
static createEmptyKey(): bytes32 {
return new Uint8Array(32);
}
protected isEmptyKey(k: bytes32): boolean {
const emptyKey = CipherState.createEmptyKey();
return uint8ArrayEquals(emptyKey, k);
}
// Encrypts a plaintext using key material in a Noise Cipher State
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
encryptWithAd(ad: Uint8Array, plaintext: Uint8Array): Uint8Array {
this.n.assertValue();
let ciphertext = new Uint8Array();
if (this.hasKey()) {
// If an encryption key is set in the Cipher state, we proceed with encryption
ciphertext = chaCha20Poly1305Encrypt(
plaintext,
this.n.getBytes(),
ad,
this.k
);
this.n.increment();
this.n.assertValue();
console.trace("encryptWithAd", ciphertext, this.n.getUint64() - 1);
} else {
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
ciphertext = plaintext;
console.debug(
"encryptWithAd called with no encryption key set. Returning plaintext."
);
}
return ciphertext;
}
// Decrypts a ciphertext using key material in a Noise Cipher State
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
decryptWithAd(ad: Uint8Array, ciphertext: Uint8Array): Uint8Array {
this.n.assertValue();
if (this.hasKey()) {
const plaintext = chaCha20Poly1305Decrypt(
ciphertext,
this.n.getBytes(),
ad,
this.k
);
if (!plaintext) {
throw "decryptWithAd failed";
}
return plaintext;
} else {
// Otherwise we return the input ciphertext according to specification
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object
console.debug(
"decryptWithAd called with no encryption key set. Returning ciphertext."
);
return ciphertext;
}
}
// Sets the nonce of a Cipher State
setNonce(nonce: Nonce): void {
this.n = nonce;
}
// Sets the key of a Cipher State
setCipherStateKey(key: bytes32): void {
this.k = key;
}
// Gets the key of a Cipher State
getKey(): bytes32 {
return this.k;
}
// Gets the nonce of a Cipher State
getNonce(): Nonce {
return this.n;
}
}
function hashProtocol(name: string): Uint8Array {
// If protocol_name is less than or equal to HASHLEN bytes in length,
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
// Otherwise sets h = HASH(protocol_name).
const protocolName = uint8ArrayFromString(name, "utf-8");
if (protocolName.length <= 32) {
const h = new Uint8Array(32);
h.set(protocolName);
return h;
} else {
return hashSHA256(protocolName);
}
}
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h
export class SymmetricState {
cs: CipherState;
ck: bytes32; // chaining key
h: bytes32; // handshake hash
constructor(hsPattern: HandshakePattern) {
this.h = hashProtocol(hsPattern.name);
this.ck = this.h;
this.cs = new CipherState();
}
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Updates a Symmetric state chaining key and symmetric state
mixKey(inputKeyMaterial: Uint8Array): void {
// We derive two keys using HKDF
const [ck, tempK] = getHKDF(this.ck, inputKeyMaterial);
// We update ck and the Cipher state's key k using the output of HDKF
this.cs = new CipherState(tempK);
this.ck = ck;
console.trace("mixKey", this.ck, this.cs.k);
}
// MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Hashes data into a Symmetric State's handshake hash value h
mixHash(data: Uint8Array): void {
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
this.h = hashSHA256(
uint8ArrayConcat([this.h, data], this.h.length + data.length)
);
console.trace("mixHash", this.h);
}
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines MixKey and MixHash
mixKeyAndHash(inputKeyMaterial: Uint8Array): void {
// Derives 3 keys using HKDF, the chaining key and the input key material
const [tempk0, tempk1, tempk2] = getHKDF(this.ck, inputKeyMaterial);
// Sets the chaining key
this.ck = tempk0;
// Updates the handshake hash value
this.mixHash(tempk1);
// Updates the Cipher state's key
// Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes."
this.cs = new CipherState(tempk2);
}
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines encryptWithAd and mixHash
// Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
encryptAndHash(
plaintext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
// The additional data
const ad = uint8ArrayConcat([this.h, extraAd]);
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintex
const ciphertext = this.cs.encryptWithAd(ad, plaintext);
// We call mixHash over the result
this.mixHash(ciphertext);
return ciphertext;
}
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines decryptWithAd and mixHash
decryptAndHash(
ciphertext: Uint8Array,
extraAd: Uint8Array = new Uint8Array()
): Uint8Array {
// The additional data
const ad = uint8ArrayConcat([this.h, extraAd]);
// Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
const plaintext = this.cs.decryptWithAd(ad, ciphertext);
// According to specification, the ciphertext enters mixHash (and not the plaintext)
this.mixHash(ciphertext);
return plaintext;
}
// Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
split(): { cs1: CipherState; cs2: CipherState } {
// Derives 2 keys using HKDF and the chaining key
const [tempk1, tempk2] = getHKDF(this.ck, new Uint8Array(0));
// Returns a tuple of two Cipher States initialized with the derived keys
return {
cs1: new CipherState(tempk1),
cs2: new CipherState(tempk2),
};
}
// Gets the chaining key field of a Symmetric State
getChainingKey(): bytes32 {
return this.ck;
}
// Gets the handshake hash field of a Symmetric State
getHandshakeHash(): bytes32 {
return this.h;
}
// Gets the Cipher State field of a Symmetric State
getCipherState(): CipherState {
return this.cs;
}
}

54
src/nonce.ts Normal file
View File

@ -0,0 +1,54 @@
import type { bytes, uint64 } from "./@types/basic";
export const MIN_NONCE = 0;
// For performance reasons, the nonce is represented as a JS `number`
// Although JS `number` can safely represent integers up to 2 ** 53 - 1, we choose to only use
// 4 bytes to store the data for performance reason.
// 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.
// this MAX_NONCE is still a large number of messages, so the practical effect of this is negligible.
export const MAX_NONCE = 0xffffffff;
const ERR_MAX_NONCE =
"Cipherstate has reached maximum n, a new handshake must be performed";
/**
* The nonce is an uint that's increased over time.
* Maintaining different representations help improve performance.
*/
export class Nonce {
private n: uint64;
private readonly bytes: bytes;
private readonly view: DataView;
constructor(n = MIN_NONCE) {
this.n = n;
this.bytes = new Uint8Array(12);
this.view = new DataView(
this.bytes.buffer,
this.bytes.byteOffset,
this.bytes.byteLength
);
this.view.setUint32(4, n, true);
}
increment(): void {
this.n++;
// Even though we're treating the nonce as 8 bytes, RFC7539 specifies 12 bytes for a nonce.
this.view.setUint32(4, this.n, true);
}
getBytes(): bytes {
return this.bytes;
}
getUint64(): uint64 {
return this.n;
}
assertValue(): void {
if (this.n > MAX_NONCE) {
throw new Error(ERR_MAX_NONCE);
}
}
}

156
src/patterns.ts Normal file
View File

@ -0,0 +1,156 @@
// The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
export enum NoiseTokens {
e = "e",
s = "s",
es = "es",
ee = "ee",
se = "se",
ss = "ss",
psk = "psk",
}
// The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
// as in http://www.noiseprotocol.org/noise.html#alice-and-bob
export enum MessageDirection {
r = "->",
l = "<-",
}
// The pre message pattern consisting of a message direction and some Noise tokens, if any.
// (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
export class PreMessagePattern {
direction: MessageDirection;
tokens: Array<NoiseTokens>;
constructor(direction: MessageDirection, tokens: Array<NoiseTokens>) {
this.direction = direction;
this.tokens = tokens;
}
equals(b: PreMessagePattern): boolean {
return (
this.direction == b.direction &&
this.tokens.length === b.tokens.length &&
this.tokens.every((val, index) => val === b.tokens[index])
);
}
}
// The message pattern consisting of a message direction and some Noise tokens
// All Noise tokens are allowed
export class MessagePattern {
direction: MessageDirection;
tokens: Array<NoiseTokens>;
constructor(direction: MessageDirection, tokens: Array<NoiseTokens>) {
this.direction = direction;
this.tokens = tokens;
}
}
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
export class HandshakePattern {
name: string;
preMessagePatterns: Array<PreMessagePattern>;
messagePatterns: Array<MessagePattern>;
constructor(
name: string,
preMessagePatterns: Array<PreMessagePattern>,
messagePatterns: Array<MessagePattern>
) {
this.name = name;
this.preMessagePatterns = preMessagePatterns;
this.messagePatterns = messagePatterns;
}
}
// Constants (supported protocols)
export const EmptyPreMessage = new Array<PreMessagePattern>();
// Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
export const NoiseHandshakePatterns = {
K1K1: new HandshakePattern(
"Noise_K1K1_25519_ChaChaPoly_SHA256",
[
new PreMessagePattern(MessageDirection.r, [NoiseTokens.s]),
new PreMessagePattern(MessageDirection.l, [NoiseTokens.s]),
],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.se]),
]
),
XK1: new HandshakePattern(
"Noise_XK1_25519_ChaChaPoly_SHA256",
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.s])],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
XX: new HandshakePattern(
"Noise_XX_25519_ChaChaPoly_SHA256",
EmptyPreMessage,
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.s,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
XXpsk0: new HandshakePattern(
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
EmptyPreMessage,
[
new MessagePattern(MessageDirection.r, [NoiseTokens.psk, NoiseTokens.e]),
new MessagePattern(MessageDirection.l, [
NoiseTokens.e,
NoiseTokens.ee,
NoiseTokens.s,
NoiseTokens.es,
]),
new MessagePattern(MessageDirection.r, [NoiseTokens.s, NoiseTokens.se]),
]
),
WakuPairing: new HandshakePattern(
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
[new PreMessagePattern(MessageDirection.l, [NoiseTokens.e])],
[
new MessagePattern(MessageDirection.r, [NoiseTokens.e, NoiseTokens.ee]),
new MessagePattern(MessageDirection.l, [NoiseTokens.s, NoiseTokens.es]),
new MessagePattern(MessageDirection.r, [
NoiseTokens.s,
NoiseTokens.se,
NoiseTokens.ss,
]),
]
),
};
// Supported Protocol ID for PayloadV2 objects
// Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
export const PayloadV2ProtocolIDs = {
"": 0,
Noise_K1K1_25519_ChaChaPoly_SHA256: 10,
Noise_XK1_25519_ChaChaPoly_SHA256: 11,
Noise_XX_25519_ChaChaPoly_SHA256: 12,
Noise_XXpsk0_25519_ChaChaPoly_SHA256: 13,
Noise_WakuPairing_25519_ChaChaPoly_SHA256: 14,
ChaChaPoly: 30,
};

28
src/publickey.ts Normal file
View File

@ -0,0 +1,28 @@
import { equals as uint8ArrayEquals } from "uint8arrays/equals";
import { bytes32 } from "./@types/basic";
// A Noise public key is a public key exchanged during Noise handshakes (no private part)
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
// or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1)
// Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
export class NoisePublicKey {
flag: number;
pk: Uint8Array;
constructor(flag: number, pk: Uint8Array) {
this.flag = flag;
this.pk = pk;
}
// Checks equality between two Noise public keys
equals(k2: NoisePublicKey): boolean {
return this.flag == k2.flag && uint8ArrayEquals(this.pk, k2.pk);
}
// Converts a public Elliptic Curve key to an unencrypted Noise public key
static to(publicKey: bytes32): NoisePublicKey {
return new NoisePublicKey(0, publicKey);
}
}

8
tsconfig.dev.json Normal file
View File

@ -0,0 +1,8 @@
{
"extends": "./tsconfig",
"compilerOptions": {
"module": "esnext",
"noEmit": true
},
"exclude": []
}

49
tsconfig.json Normal file
View File

@ -0,0 +1,49 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2020",
"outDir": "dist/",
"rootDir": "src",
"moduleResolution": "node",
"module": "es2020",
"declaration": true,
"allowJs": true,
"sourceMap": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
"resolveJsonModule": true /* Include modules imported with .json extension. */,
"tsBuildInfoFile": "dist/.tsbuildinfo",
"strict": true /* Enable all strict type-checking options. */,
/* Strict Type-Checking Options */
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
"strictFunctionTypes": true /* Enable strict checking of function types. */,
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
"forceConsistentCasingInFileNames": true,
/* Debugging Options */
"traceResolution": false /* Report module resolution log messages. */,
"listEmittedFiles": false /* Print names of generated files part of the compilation. */,
"listFiles": false /* Print names of files part of the compilation. */,
"pretty": true /* Stylize errors and messages using color and context. */,
// Due to broken types in indirect dependencies
"skipLibCheck": true,
/* Experimental Options */
// "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
// "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"lib": ["es2020", "dom"],
"types": ["node", "mocha"],
"typeRoots": ["node_modules/@types", "src/@types"]
},
"include": ["src"],
"exclude": ["src/**/*.spec.ts", "src/test_utils"],
"compileOnSave": false,
"ts-node": {
"files": true
}
}

6
tsconfig.karma.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "./tsconfig.dev",
"compilerOptions": {
"noEmit": false
}
}

10
typedoc.json Normal file
View File

@ -0,0 +1,10 @@
{
"entryPoints": ["./src/index.ts"],
"out": "build/docs",
"exclude": "**/*.spec.ts",
"excludeInternal": true,
"validation": {
"invalidLink": true,
"notExported": true
}
}