UNPKG

@foxt/js-srp

Version:

js-srp modified to add support for the SRP implementation used by Apple's iCloud.com

412 lines (376 loc) 16.8 kB
/** * A simple SRP-6a implementation. * * -- * * This code is licensed under the terms of the ISC license. * * Copyright © 2022, John Chadwick <john@jchw.io> * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ import { bigintFromBytes, bytesFromBigint, concatBytes, constantTimeCompare, fromHex, hash, Hash, hashInterleave, modPow, randomBytes, toHex, xorBytes, } from "./util.js"; /** * Prime fields used for cryptographic operations. */ interface PrimeField { g: bigint; N: bigint; n: number; } /** * Enumeration of different compatibility modes. */ export enum Mode { RFC2945 = 0, /** Implements the hash interleave. */ SRPTools = 1, GoSRP = 2, GSA = 3, } /** Known-safe prime fields. */ const knownPrimeFields = new Map<number, PrimeField>([ [1024, { g: 2n, N: 0xeeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3n, n: 128 }], [1536, { g: 2n, N: 0x9def3cafb939277ab1f12a8617a47bbbdba51df499ac4c80beeea9614b19cc4d5f4f5f556e27cbde51c6a94be4607a291558903ba0d0f84380b655bb9a22e8dcdf028a7cec67f0d08134b1c8b97989149b609e0be3bab63d47548381dbc5b1fc764e3f4b53dd9da1158bfd3e2b9c8cf56edf019539349627db2fd53d24b7c48665772e437d6c7f8ce442734af7ccb7ae837c264ae3a9beb87f8a2fe9b8b5292e5a021fff5e91479e8ce7a28c2442c6f315180f93499a234dcf76e3fed135f9bbn, n: 192 }], [2048, { g: 2n, N: 0xac6bdb41324a9a9bf166de5e1389582faf72b6651987ee07fc3192943db56050a37329cbb4a099ed8193e0757767a13dd52312ab4b03310dcd7f48a9da04fd50e8083969edb767b0cf6095179a163ab3661a05fbd5faaae82918a9962f0b93b855f97993ec975eeaa80d740adbf4ff747359d041d5c33ea71d281e446b14773bca97b43a23fb801676bd207a436c6481f1d2b9078717461a5b9d32e688f87748544523b524b0d57d5ea77a2775d2ecfa032cfbdbf52fb3786160279004e57ae6af874e7303ce53299ccc041c7bc308d82a5698f3a8d0c38271ae35f8e9dbfbb694b5c803d89f7ae435de236d525f54759b65e372fcd68ef20fa7111f9e4aff73n, n: 256 }], [3072, { g: 5n, N: 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a93ad2caffffffffffffffffn, n: 384 }], [4096, { g: 5n, N: 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c934063199ffffffffffffffffn, n: 512 }], [6144, { g: 5n, N: 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f46980c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a79715eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55cda56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b7474d6e694f91e6dcc4024ffffffffffffffffn, n: 768 }], [8192, { g: 19n, N: 0xffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aaac42dad33170d04507a33a85521abdf1cba64ecfb850458dbef0a8aea71575d060c7db3970f85a6e1e4c7abf5ae8cdb0933d71e8c94e04a25619dcee3d2261ad2ee6bf12ffa06d98a0864d87602733ec86a64521f2b18177b200cbbe117577a615d6c770988c0bad946e208e24fa074e5ab3143db5bfce0fd108e4b82d120a92108011a723c12a787e6d788719a10bdba5b2699c327186af4e23c1a946834b6150bda2583e9ca2ad44ce8dbbbc2db04de8ef92e8efc141fbecaa6287c59474e6bc05d99b2964fa090c3a2233ba186515be7ed1f612970cee2d7afb81bdd762170481cd0069127d5b05aa993b4ea988d8fddc186ffb7dc90a6c08f4df435c93402849236c3fab4d27c7026c1d4dcb2602646dec9751e763dba37bdf8ff9406ad9e530ee5db382f413001aeb06a53ed9027d831179727b0865a8918da3edbebcf9b14ed44ce6cbaced4bb1bdb7f1447e6cc254b332051512bd7af426fb8f401378cd2bf5983ca01c64b92ecf032ea15d1721d03f482d7ce6e74fef6d55e702f46980c82b5a84031900b1c9e59e7c97fbec7e8f323a97a7e36cc88be0f1d45b7ff585ac54bd407b22b4154aacc8f6d7ebf48e1d814cc5ed20f8037e0a79715eef29be32806a1d58bb7c5da76f550aa3d8a1fbff0eb19ccb1a313d55cda56c9ec2ef29632387fe8d76e3c0468043e8f663f4860ee12bf2d5b0b7474d6e694f91e6dbe115974a3926f12fee5e438777cb6a932df8cd8bec4d073b931ba3bc832b68d9dd300741fa7bf8afc47ed2576f6936ba424663aab639c5ae4f5683423b4742bf1c978238f16cbe39d652de3fdb8befc848ad922222e04a4037c0713eb57a81a23f0c73473fc646cea306b4bcbc8862f8385ddfa9d4b7fa2c087e879683303ed5bdd3a062b3cf5b3a278a66d2a13f83f44f82ddf310ee074ab6a364597e899a0255dc164f31cc50846851df9ab48195ded7ea1b1d510bd7ee74d73faf36bc31ecfa268359046f4eb879f924009438b481c6cd7889a002ed5ee382bc9190da6fc026e479558e4475677e9aa9e3050e2765694dfc81f56e880b96e7160c980dd98edd3dfffffffffffffffffn, n: 1024 }], ]); /** * Find a known-safe prime field for a given number of bits. * * @param {number} bits * @return {PrimeField} */ function findPrimeField(bits: number = 0): PrimeField { if (bits === 0) bits = 2048; const primeField = knownPrimeFields.get(bits); if (!primeField) { throw new Error(`Invalid prime field size ${bits}`); } return primeField; } /** * Returns a serialized bigint with padding to ensure it is at least n bytes. * * @param {bigint} x number to serialize * @param {number} n minimum number of bytes to return * @return {Uint8Array} buffer containing padded output */ function pad(x: bigint, n: number): Uint8Array { const b = bytesFromBigint(x); if (b.length >= n) { return b; } const z = n - b.length; const p = new Uint8Array(n); for (let i = 0; i < z; i++) { p[i] = 0; } p.set(b, z); return p; } /** * Performs basic SRP operations with a given prime field. */ export class Srp { readonly pf: PrimeField; constructor(readonly m: Mode, readonly h: Hash, bits: number = 0) { this.pf = findPrimeField(bits); } /** * Calculates the hash of a buffer and returns it as a bigint. */ async hashInt(buf: Uint8Array): Promise<bigint> { return bigintFromBytes(new Uint8Array(await hash(this.h, buf))); } /** * Calculate a verifier for the given identity + passphrase. * * @param {Uint8Array} I raw identity value * @param {Uint8Array} p raw passphrase value * @param {Uint8Array?} salt optional salt value; otherwise random * @return {Verifier} */ async verifier( I: Uint8Array, p: Uint8Array, salt?: Uint8Array ): Promise<Verifier> { const ih = this.m === Mode.GoSRP ? new Uint8Array(await hash(this.h, I)) : I; const ph = this.m === Mode.GoSRP ? new Uint8Array(await hash(this.h, p)) : p; const pf = this.pf; if (!salt) salt = randomBytes(pf.n); const x = this.m === Mode.GoSRP ? await this.hashInt(concatBytes(ih, ph, salt)) : await this.hashInt( concatBytes(salt, new Uint8Array( await hash(this.h, concatBytes(ih, new Uint8Array([0x3A]), ph) ) )) ); const v = modPow(pf.g, x, pf.N); return new Verifier({ i: ih, s: salt, v: bytesFromBigint(v), h: this.h, pf: pf, }); } /** * Initialize SRP client operations. * * @param {Uint8Array} I raw identity value * @param {Uint8Array} p raw passphrase value * @param {bigint?} a optional; private value a to use * @return {Promise<Client>} */ async newClient(I: Uint8Array, p: Uint8Array, a?: bigint): Promise<Client> { const pf = this.pf; if (!a) { a = bigintFromBytes(randomBytes(pf.n)); } return new Client({ s: this, i: this.m === Mode.GoSRP ? new Uint8Array(await hash(this.h, I)): I, p: this.m === Mode.GoSRP ? new Uint8Array(await hash(this.h, p)): p, a, A: modPow(pf.g, a, pf.N), k: await this.hashInt( concatBytes(bytesFromBigint(pf.N), pad(pf.g, Number(pf.n))) ), }); } } /** * Contains SRP verifier parameters. */ export class Verifier { readonly i: Uint8Array; readonly s: Uint8Array; readonly v: Uint8Array; readonly h: Hash; readonly pf: PrimeField; constructor(fields: { i: Uint8Array; s: Uint8Array; v: Uint8Array; h: Hash; pf: PrimeField; }) { this.i = fields.i; this.s = fields.s; this.v = fields.v; this.h = fields.h; this.pf = fields.pf; } /** * Encodes the verifier to a string. * * @returns {[string, string]} tuple of hashed identity and verifier string */ encode(): [string, string] { const ih = toHex(this.i); const b = [ this.pf.n.toString(10), this.pf.N.toString(16), this.pf.g.toString(16), this.h.toString(10), ih, toHex(this.s), toHex(this.v), ].join(":"); return [ih, b]; } } /** * Performs SRP client operations. */ export class Client { readonly s: Srp; readonly i: Uint8Array; p: Uint8Array; readonly a: bigint; readonly A: bigint; readonly k: bigint; private _K: Uint8Array; _M: Uint8Array; get K() { return this._K; } get M() { return this._M; } constructor(fields: { s: Srp; i: Uint8Array; p: Uint8Array; a: bigint; A: bigint; k: bigint; }) { this.s = fields.s; this.i = fields.i; this.p = fields.p; this.a = fields.a; this.A = fields.A; this.k = fields.k; this._K = new Uint8Array(); this._M = new Uint8Array(); } /** * Returns credentials to pass to the server. * * @returns {string} serialized credentials */ credentials(): string { return [toHex(this.i), toHex(bytesFromBigint(this.A))].join(":"); } /** * Parses serialized go-srp-style server credentials. * * @param {string} srv server credentials * @return {[Uint8Array, Uint8Array]} the salt and B values. */ parseServerCredentials(srv: string): [Uint8Array, Uint8Array] { const v = srv.split(":"); if (!v[0] || !v[1]) { throw new Error("Invalid server public key"); } const salt = fromHex(v[0]); const B = fromHex(v[1]); return [salt, B]; } /** * Generates an authenticator, given server credentials. * * @param {string} salt verifier salt * @param {Uint8Array} B server public key * @return {Promise<string>} authenticator to send to server */ async generate(salt: Uint8Array, B: Uint8Array): Promise<string> { let Bn = bigintFromBytes(B); const pf = this.s.pf; if (Bn % pf.N === 0n) { throw new Error("Invalid server public key"); } const u = await this.s.hashInt( concatBytes(pad(this.A, pf.n), pad(Bn, pf.n)) ); if (u === 0n) { throw new Error("Invalid server public key"); } const x = this.s.m === Mode.GoSRP ? await this.s.hashInt(concatBytes(this.i, this.p, salt)) : await this.s.hashInt( concatBytes(salt, new Uint8Array( await hash(this.s.h, concatBytes( this.s.m == Mode.GSA ? new Uint8Array([]) : this.i, new Uint8Array([0x3A]), this.p ) ) )) ); const t0 = modPow(pf.g, x, pf.N) * this.k; const t1 = Bn - t0; const t2 = this.a + u * x; const S = modPow(t1, t2, pf.N); // SHA_Interleave is not used by most SRP implementations. this._K = this.s.m === Mode.RFC2945 ? new Uint8Array(await hashInterleave(this.s.h, bytesFromBigint(S))) : new Uint8Array(await hash(this.s.h, bytesFromBigint(S))); if (this.s.m === Mode.GoSRP) { // Simplified construction used by Go SRP. this._M = new Uint8Array( await hash( this.s.h, concatBytes( this._K, bytesFromBigint(this.A), bytesFromBigint(Bn), this.i, salt, bytesFromBigint(pf.N), bytesFromBigint(pf.g) ) ) ); } else { this._M = new Uint8Array( await hash( this.s.h, concatBytes( xorBytes( new Uint8Array(await hash(this.s.h, bytesFromBigint(pf.N))), new Uint8Array(await hash( this.s.h, this.s.m == Mode.GSA ? pad(pf.g,pf.n) : bytesFromBigint(pf.g) )), ), new Uint8Array(await hash(this.s.h, this.i)), salt, bytesFromBigint(this.A), bytesFromBigint(Bn), this._K, ) ) ); } return toHex(this._M); } /** * Generate M2 used by GSA SRP. */ async generateM2() { if (!this.M) throw new Error("M not generated"); const M2 = new Uint8Array( await hash( this.s.h, concatBytes( bytesFromBigint(this.A), this._M, this._K ) ) ); return M2; } /** * Validates the proof returned by the server. * * @param {string} proof proof returned by the server * @returns {Promise<boolean>} true if valid, otherwise false */ async serverOk(proof: string): Promise<boolean> { const enc = new TextEncoder(); const h = enc.encode( toHex(new Uint8Array(await hash(this.s.h, concatBytes(this._K, this.M)))) ); const proofBin = enc.encode(proof); return constantTimeCompare(h, proofBin); } }