├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── README.md ├── package-lock.json ├── package.json ├── src ├── index.ts ├── lib │ ├── opaque.ts │ └── util.ts └── types │ ├── io.d.ts │ └── local.d.ts ├── test ├── opaque.test.ts └── test-io.ts ├── tsconfig.base.json ├── tsconfig.json └── tsconfig.prod.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended" 9 | ], 10 | "overrides": [ 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": [ 18 | "@typescript-eslint" 19 | ], 20 | "rules": { 21 | "indent": [ 22 | "error", 23 | 2 24 | ], 25 | "linebreak-style": [ 26 | "error", 27 | "unix" 28 | ], 29 | "quotes": [ 30 | "error", 31 | "single" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | node_modules/ 4 | coverage/ 5 | dist/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - node 5 | - lts/* 6 | 7 | cache: npm 8 | 9 | script: 10 | - npm run coveralls 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OPAQUE.js 2 | TypeScript implementation of the OPAQUE asymmetric PAKE (aPAKE) protocol 3 | 4 | ## Protocol 5 | Implementation of [this Internet Draft proposal](https://datatracker.ietf.org/doc/draft-krawczyk-cfrg-opaque). 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 25 | 26 | ## Installation 27 | 28 | You may also install this module from [npm](https://www.npmjs.com/package/@nthparty/opaque). 29 | 30 | ```shell 31 | npm install @nthparty/opaque 32 | ``` 33 | 34 | ## Calling the API 35 | 36 | The process generally works as follows: 37 | 38 | ```javascript 39 | // Each party includes the 1-out-of-n module with IO: 40 | const OT = require('@nthparty/opaque')(IO); 41 | 42 | // Login credentials never reaches the server in plaintext 43 | const user_id = 'newuser'; 44 | const password = 'correct horse battery staple'; 45 | 46 | // Sign up 47 | OPAQUE.client_register(password, user_id).then(console.debug.bind(null, 'Registered:')); 48 | 49 | // Log in for the first time and receive a session token 50 | OPAQUE.client_authenticate(password, user_id).then(console.debug.bind(null, 'Shared secret:')); 51 | 52 | // Register a new user 53 | let user = OPAQUE.server_register(); 54 | 55 | // Handle a login attempt 56 | OPAQUE.server_authenticate(user.id, user.pepper); 57 | 58 | // Result: 59 | 'Registered: true' 60 | 'Login for newuser succeeded with: 4ccdf3b8cacf08273a085c952aaf3ee83633e6afcedf4f86c00497e862f43c78' 61 | 'Shared secret: 4ccdf3b8cacf08273a085c952aaf3ee83633e6afcedf4f86c00497e862f43c78' 62 | ``` 63 | 64 | Please read [opaque.test.ts](https://github.com/nthparty/opaque/blob/main/test/opaque.test.ts) for a more detailed example, and run `npm test` to test it (requires `npm ci -also=dev` first to install dependencies). 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nthparty/opaque", 3 | "version": "0.3.0", 4 | "description": "TypeScript implementation of the OPAQUE asymmetric password authenticated key exchange (aPAKE) protocol", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc -p tsconfig.prod.json", 9 | "test": "jest --coverage", 10 | "build-and-test": "npm run-script build; npm run-script test", 11 | "coveralls": "npm test && ./node_modules/coveralls/bin/coveralls.js < coverage/lcov.info", 12 | "lint": "eslint src", 13 | "lint-fix": "eslint src --fix", 14 | "lint-all": "eslint src test --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/nthparty/opaque.git#opaque" 19 | }, 20 | "keywords": [ 21 | "asymmetric", 22 | "password", 23 | "authenticated", 24 | "key-exchange", 25 | "OPAQUE", 26 | "aPAKE", 27 | "PAKE", 28 | "JavaScript", 29 | "TypeScript", 30 | "cryptography", 31 | "cryptographic-library" 32 | ], 33 | "author": "Nth Party ", 34 | "contributors": [ 35 | { 36 | "name": "Wyatt Howe", 37 | "email": "wyatt@nthparty.com", 38 | "url": "https://nthparty.com" 39 | }, 40 | { 41 | "name": "Frederick Jansen", 42 | "email": "frederick@nthparty.com", 43 | "url": "https://nthparty.com" 44 | }, 45 | { 46 | "name": "Andrei Lapets", 47 | "email": "andrei@nthparty.com", 48 | "url": "https://nthparty.com" 49 | } 50 | ], 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/nthparty/opaque/issues" 54 | }, 55 | "homepage": "https://github.com/nthparty/opaque#readme", 56 | "dependencies": { 57 | "libsodium-wrappers-sumo": "0.7.6", 58 | "oprf": "2.0.0" 59 | }, 60 | "devDependencies": { 61 | "@types/jest": "26.0.24", 62 | "@types/libsodium-wrappers-sumo": "0.7.5", 63 | "@typescript-eslint/eslint-plugin": "^5.44.0", 64 | "@typescript-eslint/parser": "^5.44.0", 65 | "coveralls": "3.1.0", 66 | "eslint": "^8.28.0", 67 | "jest": "26.2.2", 68 | "ts-jest": "26.5.6", 69 | "typescript": "4.8.4" 70 | }, 71 | "jest": { 72 | "preset": "ts-jest", 73 | "collectCoverageFrom": [ 74 | "src/lib/*.ts", 75 | "/test/{!(test-io),}.ts" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IO } from './types/io'; 2 | import type { Opaque } from './types/local'; 3 | import OPRFSlim from 'oprf'; 4 | import opaqueFactory from './lib/opaque'; 5 | 6 | export = async (io: IO, sodium?: null | typeof import('libsodium-wrappers-sumo')): Promise => { 7 | if (sodium == null) { 8 | sodium = await import('libsodium-wrappers-sumo'); 9 | } 10 | 11 | const oprf = new OPRFSlim(/*sodium*/); 12 | const opaque = opaqueFactory(io, oprf.sodium, oprf); 13 | 14 | await oprf.ready; 15 | return opaque; 16 | }; 17 | -------------------------------------------------------------------------------- /src/lib/opaque.ts: -------------------------------------------------------------------------------- 1 | import type { IO, IOData, Tag } from '../types/io'; 2 | import type { Opaque } from '../types/local'; 3 | import type * as Sodium from 'libsodium-wrappers-sumo'; 4 | import type OPRF from 'oprf'; 5 | import utilFactory from './util'; 6 | 7 | type BoundGet = (tag: T) => Promise; 8 | type BoundGive = (tag: T, msg: IOData[T]) => void; 9 | 10 | export = (io: IO, sodium: typeof Sodium, oprf: OPRF): Opaque => { 11 | const util = utilFactory(sodium, oprf); 12 | 13 | // Sign up as a new user 14 | const clientRegister: Opaque['clientRegister'] = async (password, user_id, op_id) => { 15 | op_id = op_id + ':pake_init'; 16 | const get = io.get.bind(null, op_id) as BoundGet; 17 | const give = io.give.bind(null, op_id) as BoundGive; 18 | 19 | const password_digest = util.oprfKdf(password); 20 | give('session_id', user_id); 21 | give('password_digest', password_digest); 22 | 23 | return await get('registered'); 24 | }; 25 | 26 | // Register a new user for the first time 27 | const serverRegister: Opaque['serverRegister'] = async (t, op_id) => { 28 | op_id = op_id + ':pake_init'; 29 | const get = io.get.bind(null, op_id) as BoundGet; 30 | const give = io.give.bind(null, op_id) as BoundGive; 31 | 32 | const session_id = await get('session_id'); 33 | const password_digest = await get('password_digest'); 34 | 35 | const server_oprf_key = sodium.crypto_core_ristretto255_scalar_random(); 36 | const user_symmetric_key = util.iteratedHash(util.oprfF(server_oprf_key, password_digest), t); 37 | const secret_server_scalar = sodium.crypto_core_ristretto255_scalar_random(); 38 | const server_user_scalar = sodium.crypto_core_ristretto255_scalar_random(); 39 | const public_server_point = sodium.crypto_scalarmult_ristretto255_base(secret_server_scalar); 40 | const public_user_point = sodium.crypto_scalarmult_ristretto255_base(server_user_scalar); 41 | const asymmetric_keys_enc = { 42 | secret_user_scalar_enc: util.sodiumAeadEncrypt(user_symmetric_key, server_user_scalar), 43 | public_user_point_enc: util.sodiumAeadEncrypt(user_symmetric_key, public_user_point), 44 | public_server_point_enc: util.sodiumAeadEncrypt(user_symmetric_key, public_server_point), 45 | }; 46 | 47 | const user_record = { id: session_id, pepper: { server_oprf_key, secret_server_scalar, public_server_point, public_user_point, asymmetric_keys_enc } }; 48 | give('registered', true); 49 | 50 | return user_record; 51 | }; 52 | 53 | // Try to log in 54 | const clientAuthenticate: Opaque['clientAuthenticate'] = async (password, user_id, t, op_id) => { 55 | op_id = op_id + ':pake'; 56 | const get = io.get.bind(null, op_id) as BoundGet; 57 | const give = io.give.bind(null, op_id) as BoundGive; 58 | 59 | const blinding_scalar = sodium.crypto_core_ristretto255_scalar_random(); 60 | const client_secret_key = sodium.crypto_core_ristretto255_scalar_random(); 61 | 62 | const password_digest = util.oprfKdf(password); 63 | const _H1_x_ = util.oprfH1(password_digest); 64 | const H1_x = _H1_x_.point; 65 | const mask = _H1_x_.mask; 66 | const a = util.oprfRaise(H1_x, blinding_scalar); 67 | 68 | const ephemeral_public_user_point = sodium.crypto_scalarmult_ristretto255_base(client_secret_key); 69 | give('alpha', a); 70 | give('ephemeral_public_user_point', ephemeral_public_user_point); 71 | 72 | const b = await get('beta'); 73 | 74 | if (!sodium.crypto_core_ristretto255_is_valid_point(b)) { 75 | console.debug('client_authenticated_1 false ' + user_id); 76 | give('client_authenticated', false); 77 | throw new Error('client_authenticated_1 false'); 78 | } 79 | 80 | const asymmetric_keys_enc = await get('asymmetric_keys_enc'); 81 | const blinding_scalar_inv = sodium.crypto_core_ristretto255_scalar_invert(blinding_scalar); 82 | const user_symmetric_key = util.iteratedHash(util.oprfH(util.oprfRaise(b, blinding_scalar_inv), mask), t); 83 | const secret_user_scalar = util.sodiumAeadDecrypt(user_symmetric_key, asymmetric_keys_enc.secret_user_scalar_enc); 84 | 85 | if (!sodium.crypto_core_ristretto255_is_valid_point(secret_user_scalar)) { 86 | console.debug('client_authenticated_2 false ' + user_id); 87 | give('client_authenticated', false); 88 | throw new Error('client_authenticated_2 false'); 89 | } 90 | 91 | const public_user_point = util.sodiumAeadDecrypt(user_symmetric_key, asymmetric_keys_enc.public_user_point_enc); 92 | const public_server_point = util.sodiumAeadDecrypt(user_symmetric_key, asymmetric_keys_enc.public_server_point_enc); 93 | const ephemeral_public_server_point = await get('ephemeral_public_server_point'); 94 | const K = util.keyExchange(secret_user_scalar, client_secret_key, public_server_point, ephemeral_public_server_point, ephemeral_public_user_point, public_user_point); 95 | const SK = util.oprfF(K, util.sodiumFromByte(0)); 96 | const computed_server_authentication_token = util.oprfF(K, util.sodiumFromByte(1)); 97 | const user_authentication_token = util.oprfF(K, util.sodiumFromByte(2)); 98 | 99 | const actual_server_authentication_token = await get('server_authentication_token'); 100 | 101 | if (sodium.compare(computed_server_authentication_token, actual_server_authentication_token) !== 0) { 102 | // The comparable value of 0 means As equals __As 103 | console.debug('client_authenticated_3 false ' + user_id); 104 | give('client_authenticated', false); 105 | throw new Error('client_authenticated_3 false'); 106 | } 107 | 108 | give('user_authentication_token', user_authentication_token); 109 | 110 | const success = await get('authenticated'); 111 | if (success) { 112 | const token = sodium.to_hex(SK); 113 | return token; 114 | } else { 115 | console.debug('client_authenticated_4 false ' + user_id); 116 | give('client_authenticated', false); 117 | throw new Error('client_authenticated_4 false'); 118 | } 119 | }; 120 | 121 | // Authenticate a user 122 | const serverAuthenticate: Opaque['serverAuthenticate'] = async (user_id, pepper, op_id) => { 123 | op_id = op_id + ':pake'; 124 | const get = io.get.bind(null, op_id) as BoundGet; 125 | const give = io.give.bind(null, op_id) as BoundGive; 126 | 127 | const a = await get('alpha'); 128 | if (!sodium.crypto_core_ristretto255_is_valid_point(a)) { 129 | console.debug('Authentication failed. Alpha is not a group element.'); 130 | give('authenticated', false); 131 | throw new Error('Authentication failed. Alpha is not a group element.'); 132 | } 133 | const ephemeral_secret_client_scalar = sodium.crypto_core_ristretto255_scalar_random(); 134 | const b = util.oprfRaise(a, pepper.server_oprf_key); 135 | const ephemeral_public_server_point = sodium.crypto_scalarmult_ristretto255_base(ephemeral_secret_client_scalar); 136 | 137 | const ephemeral_public_user_point = await get('ephemeral_public_user_point'); 138 | const K = util.keyExchange(pepper.secret_server_scalar, ephemeral_secret_client_scalar, pepper.public_user_point, ephemeral_public_user_point, ephemeral_public_server_point, pepper.public_server_point); 139 | const SK = util.oprfF(K, util.sodiumFromByte(0)); 140 | const server_authentication_token = util.oprfF(K, util.sodiumFromByte(1)); 141 | const valid_user_authentication_token = util.oprfF(K, util.sodiumFromByte(2)); 142 | 143 | give('beta', b); 144 | give('ephemeral_public_server_point', ephemeral_public_server_point); 145 | give('asymmetric_keys_enc', pepper.asymmetric_keys_enc); 146 | give('server_authentication_token', server_authentication_token); 147 | 148 | const user_authentication_token_from_client = await get('user_authentication_token'); 149 | if (sodium.compare(valid_user_authentication_token, user_authentication_token_from_client) === 0) { 150 | // The comparable value of 0 means equality 151 | give('authenticated', true); 152 | const token = sodium.to_hex(SK); 153 | return token; 154 | } else { 155 | console.debug('Authentication failed. Wrong password for ' + user_id); 156 | give('authenticated', false); 157 | throw new Error('Authentication failed. Wrong password for ' + user_id); 158 | } 159 | }; 160 | 161 | return { 162 | clientRegister, 163 | serverRegister, 164 | clientAuthenticate, 165 | serverAuthenticate, 166 | }; 167 | }; 168 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import type { Ciphertext } from '../types/local'; 2 | import type OPRF from 'oprf'; 3 | import type * as Sodium from 'libsodium-wrappers-sumo'; 4 | 5 | interface Utils { 6 | oprfF: (k: Uint8Array, x: string | Uint8Array) => Uint8Array; 7 | oprfKdf: (pwd: string) => Uint8Array; 8 | oprfH: (x: Uint8Array, m: Uint8Array) => Uint8Array; 9 | oprfH1: (x: Uint8Array) => IMaskedData; 10 | oprfRaise: (x: Uint8Array, y: Uint8Array) => Uint8Array; 11 | keyExchange: (p: Uint8Array, x: Uint8Array, P: Uint8Array, X: Uint8Array, X1: Uint8Array, P1: Uint8Array) => Uint8Array; 12 | iteratedHash: (x: Uint8Array, t?: number) => Uint8Array; 13 | sodiumFromByte: (n: number) => Uint8Array; 14 | sodiumAeadEncrypt: (key: Uint8Array, plaintext: string | Uint8Array) => Ciphertext; 15 | sodiumAeadDecrypt: (key: Uint8Array, ciphertext: Ciphertext) => Uint8Array; 16 | } 17 | 18 | type IMaskedData = ReturnType; 19 | 20 | export = (sodium: typeof Sodium, oprf: OPRF) => { 21 | const sodiumAeadEncrypt: Utils['sodiumAeadEncrypt'] = (key, plaintext) => { 22 | const raw_ciphertext = sodium.crypto_aead_chacha20poly1305_encrypt( 23 | plaintext, 24 | null, 25 | null, 26 | new Uint8Array(8), 27 | key 28 | ); 29 | const mac_tag = sodium.crypto_auth_hmacsha512(raw_ciphertext, key); 30 | return { mac_tag, body: raw_ciphertext }; 31 | }; 32 | 33 | const sodiumAeadDecrypt: Utils['sodiumAeadDecrypt'] = (key, ciphertext) => { 34 | if (sodium.crypto_auth_hmacsha512_verify(ciphertext.mac_tag, ciphertext.body, key)) { 35 | try { 36 | return sodium.crypto_aead_chacha20poly1305_decrypt( 37 | null, 38 | ciphertext.body, 39 | null, 40 | new Uint8Array(8), 41 | key 42 | ); 43 | } catch (_) { 44 | return sodiumFromByte(255); 45 | } 46 | } else { 47 | throw new Error( 48 | 'Invalid Message Authentication Code. Someone may have tampered with the ciphertext.' 49 | ); 50 | } 51 | }; 52 | 53 | const oprfKdf: Utils['oprfKdf'] = (pwd) => oprf.hashToPoint(pwd); 54 | const oprfH: Utils['oprfH'] = (x, m) => oprf.unmaskPoint(x, m); 55 | const oprfH1: Utils['oprfH1'] = (x) => oprf.maskPoint(x); 56 | const oprfRaise: Utils['oprfRaise'] = (x, y) => oprf.scalarMult(x, y); 57 | const genericHash = (x: Uint8Array): Uint8Array => sodium.crypto_core_ristretto255_from_hash(x); 58 | const iteratedHash: Utils['iteratedHash'] = (x, t = 1000) => { 59 | return sodium.crypto_generichash(x.length, t === 1 ? x : iteratedHash(x, t - 1)); 60 | }; 61 | 62 | const oprfF: Utils['oprfF'] = (k, x) => { 63 | if (!sodium.crypto_core_ristretto255_is_valid_point(x) || !(x instanceof Uint8Array) || sodium.is_zero(x)) { 64 | // The type-cast here assumes that the value always gets passed to 65 | // `encodeURIComponent`, which coerces `Uint8Array` objects to strings anyway: 66 | x = oprf.hashToPoint(x as string); 67 | } 68 | 69 | const _H1_x_ = oprfH1(x); 70 | const H1_x = _H1_x_.point; 71 | const mask = _H1_x_.mask; 72 | 73 | const H1_x_k = oprfRaise(H1_x, k); 74 | 75 | const unmasked = oprfH(H1_x_k, mask); 76 | 77 | return unmasked; 78 | }; 79 | 80 | const sodiumFromByte: Utils['sodiumFromByte'] = (n) => { 81 | return new Uint8Array(32).fill(n); 82 | }; 83 | 84 | const keyExchange: Utils['keyExchange'] = (p, x, P, X, X1, P1) => { 85 | // Note: P1 and X1 to be used in a future authentication feature. The below (unauthenticated) key exchange suffices for now. 86 | const kx = oprf.scalarMult(X, x); 87 | const kp = oprf.scalarMult(P, p); 88 | const k = genericHash(sodium.crypto_core_ristretto255_add(kx, kp)); 89 | return k; 90 | }; 91 | 92 | return { 93 | oprfF, 94 | oprfKdf, 95 | oprfH, 96 | oprfH1, 97 | oprfRaise, 98 | keyExchange, 99 | iteratedHash, 100 | sodiumFromByte, 101 | sodiumAeadEncrypt, 102 | sodiumAeadDecrypt, 103 | }; 104 | }; 105 | -------------------------------------------------------------------------------- /src/types/io.d.ts: -------------------------------------------------------------------------------- 1 | import type { AsymmetricKeysEncrypted } from './local'; 2 | 3 | export interface IOData { 4 | session_id: string; 5 | password_digest: Uint8Array; 6 | registered: boolean; 7 | authenticated: boolean; 8 | client_authenticated: boolean; 9 | alpha: Uint8Array; 10 | beta: Uint8Array; 11 | asymmetric_keys_enc: AsymmetricKeysEncrypted; 12 | ephemeral_public_user_point: Uint8Array; 13 | ephemeral_public_server_point: Uint8Array; 14 | server_authentication_token: Uint8Array; 15 | user_authentication_token: Uint8Array; 16 | } 17 | 18 | export type Tag = keyof IOData; 19 | export type IOValue = IOData[Tag]; 20 | 21 | export interface IO { 22 | get: (op_id: string | undefined, tag: T) => Promise; 23 | give: (op_id: string | undefined, tag: T, msg: IOData[T]) => void; 24 | } 25 | -------------------------------------------------------------------------------- /src/types/local.d.ts: -------------------------------------------------------------------------------- 1 | export interface Opaque { 2 | /** 3 | * Sign up as a new user 4 | */ 5 | clientRegister: (password: string, user_id: string, op_id?: string) => Promise; 6 | 7 | /** 8 | * Register a new user for the first time 9 | */ 10 | serverRegister: (t?: number, op_id?: string) => Promise; 11 | 12 | /** 13 | * Try to log in 14 | */ 15 | clientAuthenticate: ( 16 | password: string, 17 | user_id: string, 18 | t?: number, 19 | op_id?: string 20 | ) => Promise; 21 | 22 | /** 23 | * Authenticate a user 24 | */ 25 | serverAuthenticate: (user_id: string, pepper: Pepper, op_id?: string) => Promise; 26 | } 27 | 28 | export interface UserRecord { 29 | id: string; 30 | pepper: Pepper; 31 | } 32 | 33 | export interface Pepper { 34 | server_oprf_key: Uint8Array; 35 | secret_server_scalar: Uint8Array; 36 | public_server_point: Uint8Array; 37 | public_user_point: Uint8Array; 38 | asymmetric_keys_enc: AsymmetricKeysEncrypted; 39 | } 40 | 41 | export interface AsymmetricKeysEncrypted { 42 | secret_user_scalar_enc: Ciphertext; 43 | public_user_point_enc: Ciphertext; 44 | public_server_point_enc: Ciphertext; 45 | } 46 | 47 | export interface Ciphertext { 48 | mac_tag: Uint8Array; 49 | body: Uint8Array; 50 | } 51 | -------------------------------------------------------------------------------- /test/opaque.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This is the setup for authenticated PAKE between a client and server. 3 | * In this test, a new user approaches the sever and registers an account. 4 | * Then the connection is reset, and the user attempts to log in. 5 | */ 6 | import type { Pepper } from '../src/types/local'; 7 | import IO from './test-io'; 8 | import opaqueFactory from '../src/index'; 9 | const _OPAQUE = opaqueFactory(IO); 10 | 11 | test('end-to-end working flow', done => { 12 | workflow(true, done); 13 | }); 14 | 15 | test('end-to-end wrong pass for client authenticate flow', done => { 16 | workflow(false, done); 17 | }); 18 | 19 | const workflow = async (valid: boolean, done: (err?: unknown) => void): Promise => { 20 | const OPAQUE = await _OPAQUE; 21 | 22 | /* 23 | * Client 24 | */ 25 | const user_id = 'newuser'; 26 | const password = 'correct horse battery staple'; 27 | const wrongPass = 'correct horse battery staples'; 28 | 29 | // Sign up 30 | OPAQUE.clientRegister(password, user_id).then(console.debug.bind(null, 'Registered:')); 31 | 32 | // Log in for the first time and receive a session token 33 | if (valid) { 34 | OPAQUE.clientAuthenticate(password, user_id).then(() => { 35 | valid && console.debug.bind(null, 'Shared secret:'); 36 | }); 37 | } else { 38 | OPAQUE.clientAuthenticate(wrongPass, user_id).then(_ => _, () => { 39 | !valid && done(); 40 | }); 41 | } 42 | 43 | 44 | /* 45 | * Server 46 | */ 47 | const database: Record = {}; // Test database to show what user data gets stored 48 | 49 | // Register a new user 50 | OPAQUE.serverRegister().then(user => { 51 | database[user.id] = user.pepper; 52 | 53 | // Handle a login attempt 54 | const user_id = user.id; 55 | const pepper = user.pepper; 56 | OPAQUE.serverAuthenticate(user_id, pepper).then(token => { 57 | try { 58 | valid && expect(token).not.toBeNull(); 59 | done(); 60 | } catch (error) { 61 | done(error); 62 | } 63 | }, (error: unknown) => { 64 | !valid && expect(error).toBeDefined(); 65 | }); 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /test/test-io.ts: -------------------------------------------------------------------------------- 1 | import type { IO, IOData, IOValue, Tag } from '../src/types/io'; 2 | 3 | /* 4 | * Client-Server Communications 5 | */ 6 | const listeners: Record void> = {}; 7 | const mailbox: Record = {}; 8 | const dummy_socket = (computation_id: string): IO => ({ 9 | get: (op_id, tag) => { 10 | return new Promise(function (resolve) { 11 | const _tag = computation_id + ':' + op_id + ':' + tag; 12 | const mail = mailbox[_tag] as IOData[typeof tag] | undefined; // TODO: Factor these assertions out 13 | if (!mail) { 14 | // console.debug('io.get', _tag, 'not ready'); 15 | listeners[_tag] = resolve as (val: IOValue) => void; // TODO: Factor these assertions out 16 | } else { 17 | // console.debug('io.get', _tag, mail); 18 | resolve(mail); 19 | delete mailbox[_tag]; 20 | } 21 | }); 22 | }, 23 | give: (op_id, tag: Tag, msg: IOValue) => { 24 | const _tag = computation_id + ':' + op_id + ':' + tag; 25 | // console.debug('io.give', _tag, msg); 26 | const listener = listeners[_tag]; 27 | if (!listener) { 28 | mailbox[_tag] = msg; 29 | } else { 30 | listener(msg); 31 | delete listeners[_tag]; 32 | } 33 | }, 34 | }); 35 | 36 | export = dummy_socket('example'); 37 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "lib": ["esnext", "dom", "DOM.Iterable"], 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "useDefineForClassFields": true, 8 | "esModuleInterop": true, 9 | "declaration": true, 10 | "removeComments": false, 11 | "isolatedModules": true, 12 | "strict": true, 13 | "allowJs": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitReturns": true, 16 | "noPropertyAccessFromIndexSignature": true, 17 | "noUncheckedIndexedAccess": true, 18 | "useUnknownInCatchVariables": true, 19 | "importsNotUsedAsValues": "error", 20 | "skipLibCheck": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "baseUrl": "./test", 6 | "types": ["jest"], 7 | }, 8 | "include": ["./src/**/*.ts", "./test/**/*.ts"], 9 | "exclude": [] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "baseUrl": "./src", 5 | "outDir": "./dist", 6 | }, 7 | "include": ["./src/**/*.ts"], 8 | "exclude": ["./test"] 9 | } --------------------------------------------------------------------------------