tidecloak-js
Version:
TideCloak client side JS SDK
434 lines (400 loc) • 14.8 kB
JavaScript
//
// Tide Protocol - Infrastructure for a TRUE Zero-Trust paradigm
// Copyright (C) 2022 Tide Foundation Ltd
//
// This program is free software and is subject to the terms of
// the Tide Community Open Code License as published by the
// Tide Foundation Limited. You may modify it and redistribute
// it in accordance with and subject to the terms of that License.
// This program is distributed WITHOUT WARRANTY of any kind,
// including without any implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
// See the Tide Community Open Code License for more details.
// You should have received a copy of the Tide Community Open
// Code License along with this program.
// If not, see https://tide.org/licenses_tcoc2-0-0-en
//
// Some parts of the code were taken from @noble/curves project and are protected under the following license:
//
// The MIT License (MIT)
//
// Copyright (c) 2022 Paul Miller (https://paulmillr.com)
//
// 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.
//
import { Serialization } from "../Cryptide/index.js";
import { SHA256_Digest } from "./Hashing/Hash.js";
import { mod_inv } from "./Math.js";
import { BigIntFromByteArray, base64ToBytes, bytesToBase64 } from "./Serialization.js";
const _0n = BigInt(0);
const _1n = BigInt(1);
const _2n = BigInt(2);
const Gx = BigInt("15112221349535400772501151409588531511454012693041857206046113283949847762202");
const Gy = BigInt("46316835694926478169428394003475163141307993866256225615783033603165251855960");
const Gz = BigInt("46827403850823179245072216630277197565144205554125654976674165829533817101731");
const A = BigInt(-1);
const D = BigInt("37095705934669439343138083508754565189542113879843219016388785533085940283555");
const P = BigInt("57896044618658097711785492504343953926634992332820282019728792003956564819949");
const Order = BigInt("7237005577332262213973186563042994240857116359379907606001950938285454250989");
const SQRT_M1 = BigInt("19681161376707505956807079304988542015446066515923890162744021073123829784752");
export default class Point {
static get a() {return A};
static get d() {return D;}
static get d_FOR_Y_FROM_X() {return BigInt("20800338683988658368647408995589388737092878452977063003340006470870624536394");}
static get p() {return P;}
static get order() { return Order;}
static get g() { return new Point(Gx, Gy, _1n, Gz);}
static get infinity() {return new Point(_0n, _1n, _1n, _0n);} //infinity also known as identity for ed25519
static get SQRT_M1() {return SQRT_M1;}
/**
* @param {bigint} x
* @param {bigint} y
* @param {bigint} z
* @param {bigint} t
* @param {Uint8Array}
*/
constructor(x, y=null, z=null, t=null, compressedBytes=null) {
this.x = x;
this.y = y !== null ? y : getYfromX(x);
this.z = z !== null ? z : _1n;
this.t = t !== null ? t : mod(this.x * this.y);
this.compressedBytes = compressedBytes
}
isInfinity(){
return this.isEqual(Point.infinity);
}
isEqual(other){
const X1Z2 = mod(this.x * other.z);
const X2Z1 = mod(other.x * this.z);
const Y1Z2 = mod(this.y * other.z);
const Y2Z1 = mod(other.y * this.z);
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
}
negate(){
return new Point(mod(-this.x), this.y, this.z, mod(-this.t));
}
/**
*
* @param {bigint} num
* @returns {Point}
*/
times(num, b) {
if (typeof num !== 'bigint') throw Error("Can only multiply point by number");
if(this.isEqual(Point.g) && b){
return wNAF(num);
}
var point = new Point(this.x, this.y, this.z, this.t);
let newPoint = Point.infinity;
while (num > _0n) {
if ((num & _1n) === (_1n)) {
newPoint = newPoint.add(point);
}
point = point.double();
num = num >> _1n;
}
return newPoint;
}
/**
*
* @returns {Point}
*/
double() {
let A = mod(this.x * this.x);
let B = mod(this.y * this.y);
let C = mod(_2n * mod(this.z * this.z));
let D = mod(Point.a * A);
let x1y1 = this.x + this.y;
let E = mod(mod(x1y1 * x1y1) - A - B);
let G = D + B;
let F = G - C;
let H = D - B;
let X3 = mod(E * F);
let Y3 = mod(G * H);
let T3 = mod(E * H);
let Z3 = mod(F * G);
return new Point(X3, Y3, Z3, T3);
}
/**
* @param {Point} other
* @returns {Point}
*/
add(other) {
let A = mod((this.y - this.x) * (other.y + other.x));
let B = mod((this.y + this.x) * (other.y - other.x));
let F = mod(B - A);
if (F == _0n) return this.double();
let C = mod(this.z * _2n * other.t);
let D = mod(this.t * _2n * other.z);
let E = D + C;
let G = B + A;
let H = D - C;
let X3 = mod(E * F);
let Y3 = mod(G * H);
let T3 = mod(E * H);
let Z3 = mod(F * G);
return new Point(X3, Y3, Z3, T3);
}
/**
* @param {bigint} a
* @returns
*/
blur(a){
return this.times(a);
}
/**
* @param {bigint} a
* @returns
*/
unblur(a){
return this.times(mod_inv_p(a, Point.order));
}
/**
*
* @returns {bigint}
*/
getX(){
return mod(this.x * mod_inv_p(this.z));
}
/**
*
* @returns {bigint}
*/
getY(){
return mod(this.y * mod_inv_p(this.z));
}
/**
* @param {Uint8Array} data
* @returns {Point}
*/
static from(data){
return this.decompress(data);
}
/**
* @param {string} data
* @returns {Point}
*/
static fromB64(data){
return data == null ? null : this.decompress(base64ToBytes(data));
}
/** @returns {Uint8Array} */
toArray(){
return this.compress();
}
async hash(){
return mod(BigIntFromByteArray(await SHA256_Digest(this.toArray())));
}
/**
* @returns {string}
*/
toBase64(){
return bytesToBase64(this.toArray());
}
/**@returns {Uint8Array} */
compress(){
if(this.compressedBytes != null) {
return this.compressedBytes.slice();
}
const bytes = Serialization.BigIntToByteArray(this.getY());
bytes[31] |= this.getX() & _1n ? 0x80 : 0;
this.compressedBytes = bytes.slice();
return bytes;
}
getOpenSSHPublicKey(){
// these bytes read: 0, 0, 0, 11, "ssh-ed25519", 0, 0, 0, 32
const staticBytes = new Uint8Array([0x00, 0x00, 0x00, 0x0b, 0x73, 0x73, 0x68, 0x2d, 0x65, 0x64, 0x32, 0x35, 0x35, 0x31, 0x39, 0x00, 0x00, 0x00, 0x20]);
const keyBytes = this.toArray();
const combinedBytesB64 = bytesToBase64(ConcatUint8Arrays([staticBytes, keyBytes]));
return "ssh-ed25519 " + combinedBytesB64
}
/**
* @param {Uint8Array} point_bytes
*/
static decompress(point_bytes){
// 1. First, interpret the string as an integer in little-endian
// representation. Bit 255 of this number is the least significant
// bit of the x-coordinate and denote this value x_0. The
// y-coordinate is recovered simply by clearing this bit. If the
// resulting value is >= p, decoding fails.
const normed = point_bytes.slice();
normed[31] = point_bytes[31] & ~0x80;
const y = BigIntFromByteArray(normed);
if ( y >= Point.p) throw new Error('Decompress: Expected 0 < hex < P');
// 2. To recover the x-coordinate, the curve equation implies
// x² = (y² - 1) / (d y² + 1) (mod p). The denominator is always
// non-zero mod p. Let u = y² - 1 and v = d y² + 1.
const y2 = mod(y * y);
const u = mod(y2 - _1n);
const v = mod(Point.d * y2 + _1n);
let { isValid, value: x } = uvRatio(u, v);
if (!isValid) throw new Error('Decompress: invalid y coordinate');
// 4. Finally, use the x_0 bit to select the right square root. If
// x = 0, and x_0 = 1, decoding fails. Otherwise, if x_0 != x mod
// 2, set x <-- p - x. Return the decoded point (x,y).
const isXOdd = (x & _1n) === _1n;
const isLastByteOdd = (point_bytes[31] & 0x80) !== 0;
if (isLastByteOdd !== isXOdd) {
x = mod(-x);
}
return new Point(x, y, null, null, point_bytes.slice());
}
}
/**
* @param {bigint} a
* @param {bigint} b
* @returns {bigint}
*/
function mod(a, b = Point.p) {
var res = a % b;
return res >= _0n ? res : b + res;
}
/**
* @param {bigint} number
* @param {bigint} modulo
* @returns {bigint}
*/
function mod_inv_p(number, modulo = Point.p) {
return mod_inv(number, modulo);
}
/**
* @param {bigint} x
* @returns
*/
function getYfromX(x){
// Always get 'higher' point. Remember there are two possible values, this always picks the first one to remain consistent.
var x2 = mod(x * x);
var u = mod(_1n + x2);
var v = mod(_1n + Point.d_FOR_Y_FROM_X*x2);
return uvRatio(u, v).value;
}
/**
* @param {bigint} x
* @param {bigint} power
* @returns
*/
function pow2(x, power){
while (power-- > _0n) {
x = mod(x * x);
}
return x;
}
// Power to (p-5)/8 aka x^(2^252-3)
// Used to calculate y - the square root of y².
// Exponentiates it to very big number.
// We are unwrapping the loop because it's 2x faster.
// (2n**252n-3n).toString(2) would produce bits [250x 1, 0, 1]
// We are multiplying it bit-by-bit
/**
*
* @param {bigint} x
* @returns
*/
//works
function pow_2_252_3(x) {
const P = Point.p;
const _5n = BigInt(5);
const _10n = BigInt(10);
const _20n = BigInt(20);
const _40n = BigInt(40);
const _80n = BigInt(80);
const x2 = (x * x) % P;
const b2 = (x2 * x) % P; // x^3, 11
const b4 = (pow2(b2, _2n) * b2) % P; // x^15, 1111
const b5 = (pow2(b4, _1n) * x) % P; // x^31
const b10 = (pow2(b5, _5n) * b5) % P;
const b20 = (pow2(b10, _10n) * b10) % P;
const b40 = (pow2(b20, _20n) * b20) % P;
const b80 = (pow2(b40, _40n) * b40) % P;
const b160 = (pow2(b80, _80n) * b80) % P;
const b240 = (pow2(b160, _80n) * b80) % P;
const b250 = (pow2(b240, _10n) * b10) % P;
const pow_p_5_8 = (pow2(b250, _2n) * x) % P;
// ^ To pow to (p+3)/8, multiply it by x.
return {pow_p_5_8, b2};
}
// this method is incomplete. not ready for ed25519 point decoding
/**
*
* @param {bigint} u
* @param {bigint} v
* @returns
*/
function uvRatio(u, v) {
const v3 = mod(v * v * v); // v³
const v7 = mod(v3 * v3 * v); // v⁷
const pow = pow_2_252_3(u * v7).pow_p_5_8;
let x = mod(u * v3 * pow); // (uv³)(uv⁷)^(p-5)/8
const vx2 = mod(v * x * x); // vx²
const root1 = x; // First root candidate
const root2 = mod(x * Point.SQRT_M1); // Second root candidate
const useRoot1 = vx2 === u; // If vx² = u (mod p), x is a square root
const useRoot2 = vx2 === mod(-u); // If vx² = -u, set x <-- x * 2^((p-1)/4)
const noRoot = vx2 === mod(-u * Point.SQRT_M1); // There is no valid root, vx² = -u√(-1)
if (useRoot1) x = root1;
if (useRoot2 || noRoot) x = root2; // We return root2 anyway, for const-time
if (edIsNegative(x)) x = mod(-x);
return { isValid: useRoot1 || useRoot2, value: x };
}
// Little-endian check for first LE bit (last BE bit);
function edIsNegative(num) {
return (mod(num) & _1n) === _1n;
}
const W = 8; // Precomputes-related code. W = window size
const precompute = () => {
console.log("precomputing...")
const points = []; // 10x sign(), 2x verify(). To achieve this,
const windows = 256 / W + 1; // app needs to spend 40ms+ to calculate
let p = Point.g, b = p; // a lot of points related to base point G.
for (let w = 0; w < windows; w++) { // Points are stored in array and used
b = p; // any time Gx multiplication is done.
points.push(b); // They consume 16-32 MiB of RAM.
for (let i = 1; i < 2 ** (W - 1); i++) {
b = b.add(p);
points.push(b);
}
p = b.double(); // Precomputes don't speed-up getSharedKey,
} // which multiplies user point by scalar,
return points; // when precomputes are using base point
};
let Gpows = undefined; // precomputes for base point G
const wNAF = (n) => {
// Compared to other point mult methods,
const comp = Gpows || (Gpows = precompute()); // stores 2x less points using subtraction
const neg = (cnd, p) => { let n = p.negate(); return cnd ? n : p; }; // negate
let p = Point.infinity;
const windows = 1 + 256 / W; // W=8 17 windows
const wsize = 2 ** (W - 1); // W=8 128 window size
const mask = BigInt(2 ** W - 1); // W=8 will create mask 0b11111111
const maxNum = 2 ** W; // W=8 256
const shiftBy = BigInt(W); // W=8 8
for (let w = 0; w < windows; w++) {
const off = w * wsize;
let wbits = Number(n & mask); // extract W bits.
n >>= shiftBy; // shift number by W bits.
if (wbits > wsize) {
wbits -= maxNum;
n += 1n;
} // split if bits > max: +224 => 256-32
const off2 = off + Math.abs(wbits) - 1; // offsets, evaluate both
const cnd2 = wbits < 0; // conditions, evaluate both
if (wbits !== 0) {
p = p.add(neg(cnd2, comp[off2])); // bits are 1: add to result point
}
}
return p; // return both real and fake points for JIT
};