UNPKG

pooja-docucomb-tink-crypto

Version:

A multi-language, cross-platform library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.

779 lines (717 loc) 24.2 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Common enums. */ import {InvalidArgumentsException} from '../exception/invalid_arguments_exception'; import * as Bytes from './bytes'; /** * Supported elliptic curves. */ export enum CurveType { P256 = 1, P384, P521 } /** * Supported point format. */ export enum PointFormatType { UNCOMPRESSED = 1, COMPRESSED, // Like UNCOMPRESSED but without the \x04 prefix. Crunchy uses this format. // DO NOT USE unless you are a Crunchy user moving to Tink. DO_NOT_USE_CRUNCHY_UNCOMPRESSED } /** * Supported ECDSA signature encoding. */ export enum EcdsaSignatureEncodingType { // The DER signature is encoded using ASN.1 // (https://tools.ietf.org/html/rfc5480#appendix-A): // ECDSA-Sig-Value :: = SEQUENCE { r INTEGER, s INTEGER }. In particular, the // encoding is: // 0x30 || totalLength || 0x02 || r's length || r || 0x02 || s's length || s. DER = 1, // The IEEE_P1363 signature's format is r || s, where r and s are zero-padded // and have the same size in bytes as the order of the curve. For example, for // NIST P-256 curve, r and s are zero-padded to 32 bytes. IEEE_P1363 } /** * Transform an ECDSA signature in DER encoding to IEEE P1363 encoding. * * @param der the ECDSA signature in DER encoding * @param ieeeLength the length of the ECDSA signature in IEEE * encoding. This is usually 2 * size of the elliptic curve field. * @return ECDSA signature in IEEE encoding */ export function ecdsaDer2Ieee(der: Uint8Array, ieeeLength: number): Uint8Array { if (!isValidDerEcdsaSignature(der)) { throw new InvalidArgumentsException('invalid DER signature'); } if (!Number.isInteger(ieeeLength) || ieeeLength < 0) { throw new InvalidArgumentsException( 'ieeeLength must be a nonnegative integer'); } const ieee = new Uint8Array(ieeeLength); const length = der[1] & 255; let offset = 1 + /* 0x30 */ 1; /* totalLength */ if (length >= 128) { offset++; } // Long form length offset++; // 0x02 const rLength = der[offset++]; let extraZero = 0; if (der[offset] === 0) { extraZero = 1; } const rOffset = ieeeLength / 2 - rLength + extraZero; ieee.set(der.subarray(offset + extraZero, offset + rLength), rOffset); offset += rLength + /* r byte array */ 1; /* 0x02 */ const sLength = der[offset++]; extraZero = 0; if (der[offset] === 0) { extraZero = 1; } const sOffset = ieeeLength - sLength + extraZero; ieee.set(der.subarray(offset + extraZero, offset + sLength), sOffset); return ieee; } /** * Transform an ECDSA signature in IEEE 1363 encoding to DER encoding. * * @param ieee the ECDSA signature in IEEE encoding * @return ECDSA signature in DER encoding */ export function ecdsaIeee2Der(ieee: Uint8Array): Uint8Array { if (ieee.length % 2 != 0 || ieee.length == 0 || ieee.length > 132) { throw new InvalidArgumentsException( 'Invalid IEEE P1363 signature encoding. Length: ' + ieee.length); } const r = toUnsignedBigNum(ieee.subarray(0, ieee.length / 2)); const s = toUnsignedBigNum(ieee.subarray(ieee.length / 2, ieee.length)); let offset = 0; const length = 1 + 1 + r.length + 1 + 1 + s.length; let der; if (length >= 128) { der = new Uint8Array(length + 3); der[offset++] = 48; der[offset++] = 128 + 1; der[offset++] = length; } else { der = new Uint8Array(length + 2); der[offset++] = 48; der[offset++] = length; } der[offset++] = 2; der[offset++] = r.length; der.set(r, offset); offset += r.length; der[offset++] = 2; der[offset++] = s.length; der.set(s, offset); return der; } /** * Validate that the ECDSA signature is in DER encoding, based on * https://github.com/bitcoin/bips/blob/master/bip-0066.mediawiki. * * @param sig an ECDSA siganture */ export function isValidDerEcdsaSignature(sig: Uint8Array): boolean { // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] // * total-length: 1-byte or 2-byte length descriptor of everything that // follows. // * R-length: 1-byte length descriptor of the R value that follows. // * R: arbitrary-length big-endian encoded R value. It must use the shortest // possible encoding for a positive integers (which means no null bytes at // the start, except a single one when the next byte has its highest bit // set). // * S-length: 1-byte length descriptor of the S value that follows. // * S: arbitrary-length big-endian encoded S value. The same rules apply. /* S */ if (sig.length < 1 + /* 0x30 */ 1 + /* total-length */ 1 + /* 0x02 */ 1 + /* R-length */ 1 + /* R */ 1 + /* 0x02 */ 1 + /* S-length */ 1) { // Signature is too short. return false; } // Checking bytes from left to right. // byte #1: a signature is of type 0x30 (compound). if (sig[0] != 48) { return false; } // byte #2 and maybe #3: the total length of the signature. let totalLen = sig[1] & 255; let totalLenLen = 1; // the length of the total length field, could be 2-byte. if (totalLen == 129) { // The signature is >= 128 bytes thus total length field is in long-form // encoding and occupies 2 bytes. totalLenLen = 2; // byte #3 is the total length. totalLen = sig[2] & 255; if (totalLen < 128) { // Length in long-form encoding must be >= 128. return false; } } else if (totalLen == 128 || totalLen > 129) { // Impossible values for the second byte. return false; } // Make sure the length covers the entire sig. if (totalLen != sig.length - 1 - totalLenLen) { return false; } // Start checking R. // Check whether the R element is an integer. if (sig[1 + totalLenLen] != 2) { return false; } // Extract the length of the R element. const rLen = sig[1 + /* 0x30 */ totalLenLen + 1] & /* 0x02 */ 255; // Make sure the length of the S element is still inside the signature. if (1 + /* 0x30 */ totalLenLen + 1 + /* 0x02 */ 1 + /* rLen */ rLen + 1 >= /* 0x02 */ sig.length) { return false; } // Zero-length integers are not allowed for R. if (rLen == 0) { return false; } // Negative numbers are not allowed for R. if ((sig[3 + totalLenLen] & 255) >= 128) { return false; } // Null bytes at the start of R are not allowed, unless R would // otherwise be interpreted as a negative number. if (rLen > 1 && sig[3 + totalLenLen] == 0 && (sig[4 + totalLenLen] & 255) < 128) { return false; } // Start checking S. // Check whether the S element is an integer. if (sig[3 + totalLenLen + rLen] != 2) { return false; } // Extract the length of the S element. const sLen = sig[1 + /* 0x30 */ totalLenLen + 1 + /* 0x02 */ 1 + /* rLen */ rLen + 1] & /* 0x02 */ 255; // Verify that the length of the signature matches the sum of the length of // the elements. if (1 + /* 0x30 */ totalLenLen + 1 + /* 0x02 */ 1 + /* rLen */ rLen + 1 + /* 0x02 */ 1 + /* sLen */ sLen != sig.length) { return false; } // Zero-length integers are not allowed for S. if (sLen == 0) { return false; } // Negative numbers are not allowed for S. if ((sig[5 + totalLenLen + rLen] & 255) >= 128) { return false; } // Null bytes at the start of S are not allowed, unless S would // otherwise be interpreted as a negative number. if (sLen > 1 && sig[5 + totalLenLen + rLen] == 0 && (sig[6 + totalLenLen + rLen] & 255) < 128) { return false; } return true; } /** * Transform a big integer in big endian to minimal unsigned form which has * no extra zero at the beginning except when the highest bit is set. * */ function toUnsignedBigNum(bytes: Uint8Array): Uint8Array { // Remove zero prefixes. let start = 0; while (start < bytes.length && bytes[start] == 0) { start++; } if (start == bytes.length) { start = bytes.length - 1; } let extraZero = 0; // If the 1st bit is not zero, add 1 zero byte. if ((bytes[start] & 128) == 128) { // Add extra zero. extraZero = 1; } const res = new Uint8Array(bytes.length - start + extraZero); res.set(bytes.subarray(start), extraZero); return res; } export function curveToString(curve: CurveType): string { switch (curve) { case CurveType.P256: return 'P-256'; case CurveType.P384: return 'P-384'; case CurveType.P521: return 'P-521'; } } export function curveFromString(curve: string): CurveType { switch (curve) { case 'P-256': return CurveType.P256; case 'P-384': return CurveType.P384; case 'P-521': return CurveType.P521; } throw new InvalidArgumentsException('unknown curve: ' + curve); } /** Helper method for unit tests. */ export function formatFromString(format: string): PointFormatType { switch (format) { case 'UNCOMPRESSED': return PointFormatType.UNCOMPRESSED; case 'DO_NOT_USE_CRUNCHY_UNCOMPRESSED': return PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED; case 'COMPRESSED': return PointFormatType.COMPRESSED; default: throw new InvalidArgumentsException('unknown format: ' + format); } } export function pointEncode( curve: string, format: PointFormatType, point: JsonWebKey): Uint8Array { const fieldSize = fieldSizeInBytes(curveFromString(curve)); switch (format) { case PointFormatType.UNCOMPRESSED: { const {x, y} = point; if (x === undefined) { throw new InvalidArgumentsException('x must be provided'); } if (y === undefined) { throw new InvalidArgumentsException('y must be provided'); } const result = new Uint8Array(1 + 2 * fieldSize); result[0] = 4; result.set( /* opt_webSafe = */ Bytes.fromBase64(x, true), 1); result.set( /* opt_webSafe = */ Bytes.fromBase64(y, true), 1 + fieldSize); return result; } case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED: { const {x, y} = point; if (x === undefined) { throw new InvalidArgumentsException('x must be provided'); } if (y === undefined) { throw new InvalidArgumentsException('y must be provided'); } let decodedX = Bytes.fromBase64(x, /* opt_webSafe = */ true); let decodedY = Bytes.fromBase64(y, /* opt_webSafe = */ true); if (decodedX.length > fieldSize) { // x has leading 0's, strip them. decodedX = decodedX.slice(decodedX.length - fieldSize, decodedX.length); } if (decodedY.length > fieldSize) { // y has leading 0's, strip them. decodedY = decodedY.slice(decodedY.length - fieldSize, decodedY.length); } const result = new Uint8Array(2 * fieldSize); result.set(decodedX, 0); result.set(decodedY, fieldSize); return result; } case PointFormatType.COMPRESSED: { const {x, y} = point; if (x === undefined) { throw new InvalidArgumentsException('x must be provided'); } if (y === undefined) { throw new InvalidArgumentsException('y must be provided'); } let decodedX = Bytes.fromBase64(x, /* opt_webSafe = */ true); let decodedY = Bytes.fromBase64(y, /* opt_webSafe = */ true); if (decodedX.length > fieldSize) { // x has leading 0's, strip them. decodedX = decodedX.slice(decodedX.length - fieldSize, decodedX.length); } if (decodedY.length > fieldSize) { // y has leading 0's, strip them. decodedY = decodedY.slice(decodedY.length - fieldSize, decodedY.length); } const result = new Uint8Array(1 + fieldSize); result.set(decodedX, /* offset = */ 1 + fieldSize - decodedX.length); result[0] = testBit(byteArrayToInteger(decodedY), 0) ? 3 : 2; return result; } default: throw new InvalidArgumentsException('invalid format'); } } function getModulus(curve: CurveType): bigint { // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D). switch (curve) { case CurveType.P256: return BigInt( '115792089210356248762697446949407573530086143415290314195533631308' + '867097853951'); case CurveType.P384: return BigInt( '394020061963944792122790401001436138050797392704654466679482934042' + '45721771496870329047266088258938001861606973112319'); case CurveType.P521: return BigInt( '686479766013060971498190079908139321726943530014330540939446345918' + '55431833976560521225596406614545549772963113914808580371219879' + '99716643812574028291115057151'); default: throw new InvalidArgumentsException('invalid curve'); } } function getB(curve: CurveType): bigint { // https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf (Appendix D). switch (curve) { case CurveType.P256: return BigInt( '0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b'); case CurveType.P384: return BigInt( '0xb3312fa7e23ee7e4988e056be3f82d19181d9c6efe8141120314088f5013875a' + 'c656398d8a2ed19d2a85c8edd3ec2aef'); case CurveType.P521: return BigInt( '0x051953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef10' + '9e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b5' + '03f00'); default: throw new InvalidArgumentsException('invalid curve'); } } /** Converts byte array to bigint. */ export function byteArrayToInteger(bytes: Uint8Array): bigint { return BigInt('0x' + Bytes.toHex(bytes)); } /** Converts bigint to byte array. */ export function integerToByteArray(i: bigint): Uint8Array { let input = i.toString(16); // If necessary, prepend leading zero to ensure that input length is even. input = input.length % 2 === 0 ? input : '0' + input; return Bytes.fromHex(input); } /** Returns true iff the ith bit (in lsb order) of n is set. */ function testBit(n: bigint, i: number): boolean { const m = BigInt(1) << BigInt(i); return (n & m) !== BigInt(0); } /** * Computes a modular exponent. Since JavaScript BigInt operations are not * constant-time, information about the inputs could leak. Therefore, THIS * METHOD SHOULD ONLY BE USED FOR POINT DECOMPRESSION. * * @param b base * @param exp exponent * @param p modulus * @return b^exp modulo p */ function modPow(b: bigint, exp: bigint, p: bigint): bigint { if (exp === BigInt(0)) { return BigInt(1); } let result = b; const exponentBitString = exp.toString(2); for (let i = 1; i < exponentBitString.length; ++i) { result = (result * result) % p; if (exponentBitString[i] === '1') { result = (result * b) % p; } } return result; } /** * Computes a square root modulo an odd prime. Since timing and exceptions can * leak information about the inputs, THIS METHOD SHOULD ONLY BE USED FOR * POINT DECOMPRESSION. * * @param x square * @param p prime modulus * @return square root of x modulo p */ function modSqrt(x: bigint, p: bigint): bigint { if (p <= BigInt(0)) { throw new InvalidArgumentsException('p must be positive'); } const base = x % p; // The currently supported NIST curves P-256, P-384, and P-521 all satisfy // p % 4 == 3. However, although currently a no-op, the following check // should be left in place in case other curves are supported in the future. if (testBit(p, 0) && /* istanbul ignore next */ testBit(p, 1)) { // Case p % 4 == 3 (applies to NIST curves P-256, P-384, and P-521) // q = (p + 1) / 4 const q = (p + BigInt(1)) >> BigInt(2); const squareRoot = modPow(base, q, p); if ((squareRoot * squareRoot) % p !== base) { throw new InvalidArgumentsException( 'could not find a modular square root'); } return squareRoot; } // Skipping other elliptic curve types that require Cipolla's algorithm. throw new InvalidArgumentsException('unsupported modulus value'); } /** * Computes the y-coordinate of a point on an elliptic curve given its * x-coordinate. Since timing and exceptions can leak information about the * inputs, THIS METHOD SHOULD ONLY BE USED FOR POINT DECOMPRESSION. * * @param x x-coordinate * @param lsb least significant bit of the y-coordinate * @param curve NIST curve P-256, P-384, or P-521 * @return y-coordinate */ function getY(x: bigint, lsb: boolean, curve: string): bigint { const p = getModulus(curveFromString(curve)); const a = p - BigInt(3); const b = getB(curveFromString(curve)); const rhs = (((x * x) + a) * x + b) % p; let y = modSqrt(rhs, p); if (lsb !== testBit(y, 0)) { y = (p - y) % p; } return y; } export function pointDecode( curve: string, format: PointFormatType, point: Uint8Array): JsonWebKey { const fieldSize = fieldSizeInBytes(curveFromString(curve)); switch (format) { case PointFormatType.UNCOMPRESSED: { if (point.length !== 1 + 2 * fieldSize || point[0] !== 4) { throw new InvalidArgumentsException('invalid point'); } const result = ({ 'kty': 'EC', 'crv': curve, 'x': Bytes.toBase64( new Uint8Array(point.subarray(1, 1 + fieldSize)), /* websafe */ true), 'y': Bytes.toBase64( new Uint8Array(point.subarray(1 + fieldSize, point.length)), /* websafe */ true), 'ext': true } as JsonWebKey); return result; } case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED: { if (point.length !== 2 * fieldSize) { throw new InvalidArgumentsException('invalid point'); } const result = ({ 'kty': 'EC', 'crv': curve, 'x': Bytes.toBase64( new Uint8Array(point.subarray(0, fieldSize)), /* websafe */ true), 'y': Bytes.toBase64( new Uint8Array(point.subarray(fieldSize, point.length)), /* websafe */ true), 'ext': true } as JsonWebKey); return result; } case PointFormatType.COMPRESSED: { if (point.length !== 1 + fieldSize) { throw new InvalidArgumentsException( 'compressed point has wrong length'); } if (point[0] !== 2 && point[0] !== 3) { throw new InvalidArgumentsException('invalid format'); } const lsb = (point[0] === 3); // point[0] must be 2 (false) or 3 (true). const x = byteArrayToInteger(point.subarray(1, point.length)); const p = getModulus(curveFromString(curve)); if (x < BigInt(0) || x >= p) { throw new InvalidArgumentsException('x is out of range'); } const y = getY(x, lsb, curve); const result : JsonWebKey = { 'kty': 'EC', 'crv': curve, 'x': Bytes.toBase64(integerToByteArray(x), /* websafe */ true), 'y': Bytes.toBase64(integerToByteArray(y), /* websafe */ true), 'ext': true }; return result; } default: throw new InvalidArgumentsException('invalid format'); } } export function getJsonWebKey( curve: CurveType, x: Uint8Array, y: Uint8Array, d?: Uint8Array|null): JsonWebKey { const key = ({ 'kty': 'EC', 'crv': curveToString(curve), 'x': Bytes.toBase64( x, /* websafe */ true), 'y': Bytes.toBase64( y, /* websafe */ true), 'ext': true } as JsonWebKey); if (d) { key['d'] = Bytes.toBase64( d, /* websafe */ true); } return key; } export function fieldSizeInBytes(curve: CurveType): number { switch (curve) { case CurveType.P256: return 32; case CurveType.P384: return 48; case CurveType.P521: return 66; } } export function encodingSizeInBytes( curve: CurveType, pointFormat: PointFormatType): number { switch (pointFormat) { case PointFormatType.UNCOMPRESSED: return 2 * fieldSizeInBytes(curve) + 1; case PointFormatType.COMPRESSED: return fieldSizeInBytes(curve) + 1; case PointFormatType.DO_NOT_USE_CRUNCHY_UNCOMPRESSED: return 2 * fieldSizeInBytes(curve); } } export async function computeEcdhSharedSecret( privateKey: CryptoKey, publicKey: CryptoKey): Promise<Uint8Array> { const {namedCurve}: Partial<EcKeyImportParams> = privateKey.algorithm; if (!namedCurve) { throw new InvalidArgumentsException('namedCurve must be provided'); } const ecdhParams = {'public': publicKey, ...privateKey.algorithm}; const fieldSizeInBits = 8 * fieldSizeInBytes(curveFromString(namedCurve)); const sharedSecret = await window.crypto.subtle.deriveBits( ecdhParams, privateKey, fieldSizeInBits); return new Uint8Array(sharedSecret); } export async function generateKeyPair( algorithm: 'ECDH'|'ECDSA', curve: string): Promise<CryptoKeyPair> { if (algorithm != 'ECDH' && algorithm != 'ECDSA') { throw new InvalidArgumentsException( 'algorithm must be either ECDH or ECDSA'); } const params = {'name': algorithm, 'namedCurve': curve}; const ephemeralKeyPair = await window.crypto.subtle.generateKey( params, /* extractable= */ true, algorithm == 'ECDH' ? ['deriveKey', 'deriveBits'] : ['sign', 'verify']); return ephemeralKeyPair as CryptoKeyPair; } export async function exportCryptoKey(cryptoKey: CryptoKey): Promise<JsonWebKey> { const jwk = await window.crypto.subtle.exportKey('jwk', cryptoKey); return (jwk); } export async function importPublicKey( algorithm: string, jwk: JsonWebKey): Promise<CryptoKey> { if (algorithm != 'ECDH' && algorithm != 'ECDSA') { throw new InvalidArgumentsException( 'algorithm must be either ECDH or ECDSA'); } const {crv} = jwk; if (!crv) { throw new InvalidArgumentsException('crv must be provided'); } const publicKey = await window.crypto.subtle.importKey( /* format */ 'jwk', jwk, {'name': algorithm, 'namedCurve': crv}, /* algorithm */ true, /* extractable */ algorithm == 'ECDH' ? [] : ['verify']); /* usage */ return publicKey; } export async function importPrivateKey( algorithm: string, jwk: JsonWebKey): Promise<CryptoKey> { if (algorithm != 'ECDH' && algorithm != 'ECDSA') { throw new InvalidArgumentsException( 'algorithm must be either ECDH or ECDSA'); } const {crv} = jwk; if (!crv) { throw new InvalidArgumentsException('crv must be provided'); } const privateKey = await window.crypto.subtle.importKey( /* format */ 'jwk', jwk, /* key material */ {'name': algorithm, 'namedCurve': crv}, /* algorithm */ true, /* extractable */ algorithm == 'ECDH' ? ['deriveKey', 'deriveBits'] : ['sign']); /* usage */ return privateKey; }