bwt
Better Web Token
Know someone that can security review this module?
Features
BWT
s are encrypted and authenticated- high-security
AEAD_CHACHA20_POLY1305
scheme - RFC 8439 compliant
- high-security
no crypto agility available to module users
BWT
s require a fixed set of four header claims:typ
,iat
,exp
,kid
- no opting-out
What a BWT Looks Like
QldUAAAAAWygrOCJAAABbKCs4iz5wub7BvcERzge0rd2++YzNTY2MDYzNzc5OTgx.5eHsXu2v5IUnE+DS1TVaStc=.Scb9ifOg3cEcy582KKfg7Q==
Usage
import * as bwt from "https://denopkg.com/chiefbiiko/bwt/mod.ts";
const alice = { ...bwt.generateKeyPair(), stringify: null };
const bob = { ...bwt.generateKeyPair(), parse: null };
alice.stringify = bwt.createStringify(alice.secretKey, {
kid: bob.kid,
publicKey: bob.publicKey
});
bob.parse = bwt.createParse(bob.secretKey, {
kid: alice.kid,
publicKey: alice.publicKey
});
const iat = Date.now();
const exp = iat + 1000;
const token = alice.stringify(
{ typ: bwt.Typ.BWTv0, kid: alice.kid, iat, exp },
{ info: "jwt sucks" }
);
console.log("alice seals and sends this token to bob:", token);
const contents = bob.parse(token);
console.log("bob opens it...:", JSON.stringify(contents));
API
Basics
Besides a few constants and interfaces, the module’s main exports are two factory functions, createStringify
and createParse
, that each create corresponding marshalling functions, stringify
and parse
.
As BWT
uses assymetric keys the module also exports a key generation function: generateKeyPair
. Make sure to store your private keys somewhere safe.
In case of exceptions, fx input validation or MAC verification errors, marshalling ops return null
rather than throw
ing errors (to avoid leaking sensitive information). generateKeyPair
, createStringify
, and createParse
will throw on invalid inputs though.
Find basic interfaces and constants below.
/** Typ enum indicating a BWT version @ the Header.typ field. */
export const enum Typ {
BWTv0
}
/**
* BWT header object.
*
* typ must be a supported BWT version tag, currently that is "BWTv0" only.
* iat and exp denote the issued-at and expiry ms timestamps of a token.
* kid is the public key identifier of the issuing party. base64 encoded kid
* strings are supported.
*/
export interface Header {
typ: Typ;
iat: number;
exp: number;
kid: string | Uint8Array;
}
/** BWT body object. */
export interface Body {
[key: string]: unknown;
}
/** Parsed contents of a token. */
export interface Contents {
header: Header;
body: Body;
}
/** BWT stringify function. */
export interface Stringify {
(header: Header, body: Body, peerPublicKey?: PeerPublicKey): string;
}
/** BWT parse function. */
export interface Parse {
(token: string, ...peerPublicKeys: PeerPublicKey[]): Contents;
}
/**
* BWT keypair object including a key identifier for the public key.
*
* secretKey is the 32-byte secret key.
* publicKey is the 32-byte public key.
* kid is a 16-byte key identifer for the public key.
*
* Any of the above properties can either be buffers or base64 strings.
*/
export interface KeyPair {
secretKey: string | Uint8Array;
publicKey: string | Uint8Array;
kid: string | Uint8Array;
}
/**
* BWT public key of a peer.
*
* publicKey is the 32-byte public key.
* kid is a 16-byte key identifier for the public key.
* name can be an arbitrarily encoded string or a buffer.
*
* publicKey and kid can either be buffers or base64 strings.
*/
export interface PeerPublicKey {
publicKey: string | Uint8Array;
kid: string | Uint8Array;
name?: string | Uint8Array;
}
/** Supported BWT versions. */
export const SUPPORTED_VERSIONS: Set<string> = new Set<string>(["BWTv0"]);
/** Maximum allowed number of characters of a token. */
export const MAX_TOKEN_CHARS: number = 4096;
/** Byte length of a Curve25519 secret key. */
export const SECRET_KEY_BYTES: number = 32;
/** Byte length of a Curve25519 public key. */
export const PUBLIC_KEY_BYTES: number = 32;
Core Callables
generateKeyPair(outputEncoding?: string): KeyPair
Generates a new keypair.
outputEncoding
can be set to "base64"
. By default, keys are plain Uint8Array
s.
createStringify(ownSecretKey: string | Uint8Array, defaultPeerPublicKey?: PeerPublicKey): Stringify
Creates a stringify function.
ownSecretKey
is the secret key of the keypair of the issuing party. Can be passed as a base64 string. defaultPeerPublicKey
can be the peer public key object of a party that the to-be-generated tokens are meant for. If provided, it will be used as a default, i.e. when Stringify
invocations do not receive a peer public key.
createParse(ownSecretKey: string | Uint8Array, ...defaultPeerPublicKeys: PeerPublicKey[]): Parse
Creates a parse function.
ownSecretKey
is the secret key of the keypair of the party that is going to parse and verify tokens. Can be passed as a base64 string. defaultPeerPublicKeys
can be an array of peer public key objects to be used for verification of incoming tokens. If any are specified these will be used as a default, i.e. when Parse
invocations do not receive any peer public keys to verify against.
stringify(header: Header, body: Body, peerPublicKey?: PeerPublicKey): string
Stringifies a token.
header
must contain four props:
typ
set to one of thebwt.Typ
enum variants or their string representations, e.g. “BWTv0”iat
a millisecond timestamp indicating the current timeexp
a millisecond timestamp indicating the expiry of the tokenkid
a base64 string or a binary of 16 bytes, the public key identifier of the issuing party
The above implies that every BWT
token must expire.
body
must be an object. Apart from that it can contain any type of fields.
peerPublicKey
can be specified to override a default peer public key and address a token to a specific party.
In case of invalid inputs or any other exceptions stringify
returns null
, otherwise a BWT
token.
parse(token: string, ...peerPublicKeys: PeerPublicKey[]): Contents
Parses a token.
If peerPublicKeys
consists of at least one peer public key, it takes precedence and any default peer public keys possibly passed when creating the parse function are ignored for verification of the token
.
In case of invalid inputs, exceptions, corrupt or forged tokens parse
returns null
, otherwise a BWT
header and body.
Besides format and cryptographic validation parse
verifies that the iat
and exp
claims are unsigned integers, and iat <= Date.now() < exp
.
Dear Reviewers
Quick setup:
Install
deno
:curl -fsSL https://deno.land/x/install/install.sh | sh
Get this repo:
git clone https://github.com/chiefbiiko/bwt && cd ./bwt && mkdir ./cache
Cache all dependencies and run tests:
DENO_DIR=./cache $HOME/.deno/bin/deno run ./test.ts
All relevant dependencies, aead-chacha20-poly1305
, curve25519
, std-encoding
, and base64
, are then stored in ./cache/deps/https/raw.githubusercontent.com/chiefbiiko/
and ./cache/deps/https/deno.land/x/
.
Please open an issue for your review findings. Looking forward to your feedback!
Thank you for reviewing!