diff --git a/lib/Authentication.ts b/lib/Authentication.ts index ac7d105..8fe2ea4 100644 --- a/lib/Authentication.ts +++ b/lib/Authentication.ts @@ -12,6 +12,7 @@ import uuid from 'uuid/v4'; import VerifiedRequest from './interfaces/VerifiedRequest'; import AuthenticationRequest from './interfaces/AuthenticationRequest'; import AuthenticationResponse from './interfaces/AuthenticationResponse'; +import AesCryptoSuite from './crypto/aes/AesCryptoSuite'; /** * Named arguments to construct an Authentication object @@ -49,7 +50,7 @@ export default class Authentication { this.resolver = options.resolver; this.tokenValidDurationInMinutes = options.tokenValidDurationInMinutes || Constants.defaultTokenDurationInMinutes; this.keys = options.keys; - this.factory = new CryptoFactory(options.cryptoSuites || [new RsaCryptoSuite(), new Secp256k1CryptoSuite()]); + this.factory = new CryptoFactory(options.cryptoSuites || [new AesCryptoSuite(), new RsaCryptoSuite(), new Secp256k1CryptoSuite()]); } /** diff --git a/lib/CryptoFactory.ts b/lib/CryptoFactory.ts index 244af38..9f0918c 100644 --- a/lib/CryptoFactory.ts +++ b/lib/CryptoFactory.ts @@ -1,10 +1,12 @@ -import CryptoSuite, { Encrypter, Signer, PublicKeyConstructors } from './interfaces/CryptoSuite'; +import CryptoSuite, { Encrypter, Signer, PublicKeyConstructors, SymmetricEncrypter } from './interfaces/CryptoSuite'; import { IDidDocumentPublicKey } from '@decentralized-identity/did-common-typescript'; import JweToken from './security/JweToken'; import JwsToken from './security/JwsToken'; /** A dictionary of JWA encryption algorithm names to Encrypter objects */ type EncrypterMap = {[name: string]: Encrypter}; +/** A dictionary of JWA encryption algorithm names to Encyprter objects */ +type SymmetricEncrypterMap = {[name: string]: SymmetricEncrypter}; /** A dictionary of JWA signing algorithm names to Signer objects */ type SignerMap = { [name: string]: Signer }; @@ -14,7 +16,9 @@ type SignerMap = { [name: string]: Signer }; export default class CryptoFactory { private encrypters: EncrypterMap; + private symmetricEncrypters: SymmetricEncrypterMap; private signers: SignerMap; + private defaultSymmetricAlgorithm: string; // the constructors should be factored out as they don't really relate to the pure crypto private keyConstructors: PublicKeyConstructors; @@ -23,10 +27,12 @@ export default class CryptoFactory { * Constructs a new CryptoRegistry * @param suites The suites to use for dependency injeciton */ - constructor (suites: CryptoSuite[]) { + constructor (suites: CryptoSuite[], defaultSymmetricAlgorithm?: string) { this.encrypters = {}; + this.symmetricEncrypters = {}; this.signers = {}; this.keyConstructors = {}; + this.defaultSymmetricAlgorithm = 'none'; // takes each suite (CryptoSuite objects) and maps to name of the algorithm. suites.forEach((suite) => { @@ -35,6 +41,11 @@ export default class CryptoFactory { this.encrypters[encrypterKey] = encAlgorithms[encrypterKey]; } + const symEncAlgorithms = suite.getSymmetricEncrypters(); + for (const encrypterKey in symEncAlgorithms) { + this.symmetricEncrypters[encrypterKey] = symEncAlgorithms[encrypterKey]; + } + const signerAlgorithms = suite.getSigners(); for (const signerKey in signerAlgorithms) { this.signers[signerKey] = signerAlgorithms[signerKey]; @@ -45,6 +56,15 @@ export default class CryptoFactory { this.keyConstructors[keyType] = pluginKeyConstructors[keyType]; } }); + + if (defaultSymmetricAlgorithm) { + this.defaultSymmetricAlgorithm = defaultSymmetricAlgorithm; + } else { + for (const algorithm in this.symmetricEncrypters) { + this.defaultSymmetricAlgorithm = algorithm; + break; + } + } } /** @@ -91,4 +111,20 @@ export default class CryptoFactory { getSigner (name: string): Signer { return this.signers[name]; } + + /** + * Gets the SymmetricEncrypter object given the symmetric encryption algorithm's name + * @param name The name of the algorithm + * @returns The corresponding SymmetricEncrypter, if any + */ + getSymmetricEncrypter (name: string): SymmetricEncrypter { + return this.symmetricEncrypters[name]; + } + + /** + * Gets the default symmetric encryption algorithm to use + */ + getDefaultSymmetricEncryptionAlgorithm (): string { + return this.defaultSymmetricAlgorithm; + } } diff --git a/lib/crypto/aes/AesCryptoSuite.ts b/lib/crypto/aes/AesCryptoSuite.ts new file mode 100644 index 0000000..cd37c66 --- /dev/null +++ b/lib/crypto/aes/AesCryptoSuite.ts @@ -0,0 +1,220 @@ +import CryptoSuite, { Encrypter, Signer, SymmetricEncrypter, PublicKeyConstructors } from '../../interfaces/CryptoSuite'; +import crypto from 'crypto'; + +/** + * Encrypter plugin for Advanced Encryption Standard symmetric keys + */ +export default class AesCryptoSuite implements CryptoSuite { + + getEncrypters (): { [algorithm: string]: Encrypter } { + return {}; + } + + getSigners (): { [algorithm: string]: Signer; } { + return {}; + } + + getKeyConstructors (): PublicKeyConstructors { + return {}; + } + + getSymmetricEncrypters (): { [algorithm: string]: SymmetricEncrypter } { + return { + 'A128GCM': { + encrypt: this.encryptAesGcm(128), + decrypt: this.decryptAesGcm(128) + }, + 'A192GCM': { + encrypt: this.encryptAesGcm(192), + decrypt: this.decryptAesGcm(192) + }, + 'A256GCM': { + encrypt: this.encryptAesGcm(256), + decrypt: this.decryptAesGcm(256) + }, + 'A128CBC-HS256': { + encrypt: this.encryptAesCbcHmacSha2(128, 256), + decrypt: this.decryptAesCbcHmacSha2(128, 256) + }, + 'A192CBC-HS384': { + encrypt: this.encryptAesCbcHmacSha2(192, 384), + decrypt: this.decryptAesCbcHmacSha2(192, 384) + }, + 'A256CBC-HS512': { + encrypt: this.encryptAesCbcHmacSha2(256, 512), + decrypt: this.decryptAesCbcHmacSha2(256, 512) + } + }; + } + + /** + * Given the encryption parameters, returns the AES CBC HMAC SHA2 encryption function + * @param keySize Size of the keys + * @param hashSize Size of the SHA2 hash + * @returns a SymmetricEncrypter encrypt function + */ + private encryptAesCbcHmacSha2 (keySize: number, hashSize: number): (plaintext: Buffer, additionalAuthenticatedData: Buffer) => + Promise<{ciphertext: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer}> { + return async (plaintext: Buffer, additionalAuthenticatedData: Buffer) => { + const mackey = this.generateSymmetricKey(keySize); + const enckey = this.generateSymmetricKey(keySize); + const initializationVector = this.generateInitializationVector(128); + const algorithm = `aes-${keySize}-cbc`; + const cipher = crypto.createCipheriv(algorithm, enckey, initializationVector); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), + cipher.final() + ]); + const tag = this.generateHmacTag(hashSize, keySize, mackey, additionalAuthenticatedData, initializationVector, ciphertext); + return { + ciphertext, + initializationVector, + key: Buffer.concat([mackey, enckey]), + tag + }; + }; + } + + /** + * Given the decryption parameters, returns an AES CBC HMAC SHA2 decryption function + * @param keySize Size of the keys + * @param hashSize Size of the SHA2 hash + * @returns a SymmetricEncrypter decrypt function + */ + private decryptAesCbcHmacSha2 (keySize: number, hashSize: number): + (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer) => + Promise { + return async (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer) => { + const splitLength = key.length / 2; + const mackey = key.slice(0, splitLength); + const enckey = key.slice(splitLength, key.length); + const computedTag = this.generateHmacTag(hashSize, keySize, mackey, additionalAuthenticatedData, initializationVector, ciphertext); + if (computedTag.compare(tag) !== 0) { + throw new Error('Invalid tag'); + } + const algorithm = `aes-${keySize}-cbc`; + const decipher = crypto.createDecipheriv(algorithm, enckey, initializationVector); + const plaintext = Buffer.concat([ + decipher.update(ciphertext), + decipher.final() + ]); + return plaintext; + }; + } + + /** + * Given the encryption parameters, returns the AES GCM encryption function + * @param keySize Size of the keys + * @returns a SymmetricEncrypter encrypt function + */ + private encryptAesGcm (keySize: number): (plaintext: Buffer, additionalAuthenticatedData: Buffer) => + Promise<{ciphertext: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer}> { + return async (plaintext: Buffer, additionalAuthenticatedData: Buffer) => { + const key = this.generateSymmetricKey(keySize); + const initializationVector = this.generateInitializationVector(96); + const algorithm = `aes-${keySize}-gcm`; + const cipher = crypto.createCipheriv(algorithm, key, initializationVector) as crypto.CipherGCM; + cipher.setAAD(additionalAuthenticatedData); + const ciphertext = Buffer.concat([ + cipher.update(plaintext), + cipher.final() + ]); + return { + ciphertext, + initializationVector, + key, + tag: cipher.getAuthTag() + }; + }; + } + + /** + * Given the decryption parameters, returns an AES GCM decryption function + * @param keySize Size of the keys + * @returns a SymmetricEncrypter decrypt function + */ + private decryptAesGcm (keySize: number): + (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer) => + Promise { + return async (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer) => { + const algorithm = `aes-${keySize}-gcm`; + const decipher = crypto.createDecipheriv(algorithm, key, initializationVector) as crypto.DecipherGCM; + decipher.setAAD(additionalAuthenticatedData); + decipher.setAuthTag(tag); + return Buffer.concat([ + decipher.update(ciphertext), + decipher.final() + ]); + }; + } + + /** + * Generates the HMAC Tag + * @param hashSize HMAC hash size + * @param keySize HMAC tag size + * @param mackey MAC key + * @param additionalAuthenticatedData Additional authenticated data + * @param initializationVector initialization vector + * @param ciphertext encrypted data + * @returns HMAC Tag + */ + private generateHmacTag (hashSize: number, keySize: number, mackey: Buffer, + additionalAuthenticatedData: Buffer, initializationVector: Buffer, ciphertext: Buffer): Buffer { + const mac = this.generateHmac(hashSize, mackey, additionalAuthenticatedData, initializationVector, ciphertext); + return mac.slice(0, Math.ceil(keySize / 8)); + } + + /** + * Generates the full HMac + * @param hashSize HMAC hash size + * @param mackey MAC key + * @param additionalAuthenticatedData Additional authenticated data + * @param initializationVector initialization vector + * @param ciphertext encrypted data + * @returns HMAC in full + */ + private generateHmac (hashSize: number, mackey: Buffer, + additionalAuthenticatedData: Buffer, initializationVector: Buffer, ciphertext: Buffer): Buffer { + const al = this.getAdditionalAuthenticatedDataLength(additionalAuthenticatedData); + const hmac = crypto.createHmac(`sha${hashSize}`, mackey); + hmac.update(additionalAuthenticatedData); + hmac.update(initializationVector); + hmac.update(ciphertext); + hmac.update(al); + const mac = hmac.digest(); + return mac; + } + + /** + * Gets the Additional Authenticated Data length in Big Endian notation + * @param additionalAuthenticatedData Additional authenticated data + * @return Additional Authenticated Data returned as a base64 big endian unsigned integer + */ + private getAdditionalAuthenticatedDataLength (additionalAuthenticatedData: Buffer): Buffer { + const aadLength = additionalAuthenticatedData.length * 8; + const alMsb = aadLength & 0xFFFFFFFF00000000; + const alLsb = (aadLength >> 32) & 0x00000000FFFFFFFF; + const al = Buffer.alloc(8); + al.writeUInt32BE(alMsb, 0); + al.writeUInt32BE(alLsb, 4); + return al; + } + + // these are two different functions to allow validation against RFC specs + + /** + * Generates a symmetric key + * @param bits Size in bits of the key + */ + private generateSymmetricKey (bits: number): Buffer { + return crypto.randomBytes(Math.ceil(bits / 8)); + } + + /** + * Generates an initialization vector + * @param bits Size in bits of the initialization vector + */ + private generateInitializationVector (bits: number): Buffer { + return crypto.randomBytes(Math.ceil(bits / 8)); + } +} diff --git a/lib/crypto/ec/Secp256k1CryptoSuite.ts b/lib/crypto/ec/Secp256k1CryptoSuite.ts index b0bd9fa..3b2b0d3 100644 --- a/lib/crypto/ec/Secp256k1CryptoSuite.ts +++ b/lib/crypto/ec/Secp256k1CryptoSuite.ts @@ -1,5 +1,5 @@ import EcPublicKey from './EcPublicKey'; -import CryptoSuite from '../../interfaces/CryptoSuite'; +import CryptoSuite, { SymmetricEncrypter } from '../../interfaces/CryptoSuite'; import PrivateKey from '../../security/PrivateKey'; import PublicKey from '../../security/PublicKey'; import { IDidDocumentPublicKey } from '@decentralized-identity/did-common-typescript'; @@ -10,6 +10,11 @@ const ecKey = require('ec-key'); * Encrypter plugin for Elliptic Curve P-256K1 */ export class Secp256k1CryptoSuite implements CryptoSuite { + + getSymmetricEncrypters (): { [algorithm: string]: SymmetricEncrypter } { + return {}; + } + /** Encryption with Secp256k1 keys not supported */ getEncrypters () { return {}; diff --git a/lib/crypto/rsa/RsaCryptoSuite.ts b/lib/crypto/rsa/RsaCryptoSuite.ts index 8a326b1..fd6ef6d 100644 --- a/lib/crypto/rsa/RsaCryptoSuite.ts +++ b/lib/crypto/rsa/RsaCryptoSuite.ts @@ -1,5 +1,5 @@ import RsaPublicKey from './RsaPublicKey'; -import CryptoSuite from '../../interfaces/CryptoSuite'; +import CryptoSuite, { SymmetricEncrypter } from '../../interfaces/CryptoSuite'; import { IDidDocumentPublicKey } from '@decentralized-identity/did-common-typescript'; // TODO: Create and reference TypeScript definition file for 'jwk-to-pem' const jwkToPem = require('jwk-to-pem'); @@ -12,6 +12,11 @@ import PublicKey from '../../security/PublicKey'; * Encrypter plugin for RsaSignature2018 */ export class RsaCryptoSuite implements CryptoSuite { + + getSymmetricEncrypters (): { [algorithm: string]: SymmetricEncrypter } { + return {}; + } + getEncrypters () { return { 'RSA-OAEP': { diff --git a/lib/index.ts b/lib/index.ts index 89d135a..27ae6f9 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -5,15 +5,21 @@ import { RsaCryptoSuite } from './crypto/rsa/RsaCryptoSuite'; import PrivateKeyRsa from './crypto/rsa/RsaPrivateKey'; import { Secp256k1CryptoSuite } from './crypto/ec/Secp256k1CryptoSuite'; import EcPrivateKey from './crypto/ec/EcPrivateKey'; +import AesCryptoSuite from './crypto/aes/AesCryptoSuite'; import JweToken from './security/JweToken'; import JwsToken from './security/JwsToken'; import CryptoFactory from './CryptoFactory'; import Authentication, { AuthenticationOptions } from './Authentication'; import VerifiedRequest from './interfaces/VerifiedRequest'; +import TestPrivateKey from '../tests/mocks/TestPrivateKey'; +import { TestPublicKey } from '../tests/mocks/TestPublicKey'; +import TestCryptoSuite from '../tests/mocks/TestCryptoProvider'; export { Authentication, AuthenticationOptions, VerifiedRequest }; export { CryptoSuite, Encrypter, Signer }; export { PublicKey, PrivateKey }; export { RsaCryptoSuite, PrivateKeyRsa }; export { Secp256k1CryptoSuite, EcPrivateKey }; +export { AesCryptoSuite }; export { CryptoFactory, JwsToken, JweToken }; +export { TestCryptoSuite, TestPrivateKey, TestPublicKey }; diff --git a/lib/interfaces/CryptoSuite.ts b/lib/interfaces/CryptoSuite.ts index 0f335a6..0ee9473 100644 --- a/lib/interfaces/CryptoSuite.ts +++ b/lib/interfaces/CryptoSuite.ts @@ -15,6 +15,12 @@ export default interface CryptoSuite { */ getEncrypters (): { [algorithm: string]: Encrypter }; + /** + * Get all of the symmetric encrypter algorithms from the plugin + * @returns a dictionary with the name of the algorithm for encryption/decryption as the key + */ + getSymmetricEncrypters (): { [algorithm: string]: SymmetricEncrypter }; + /** * Gets all of the Signer Algorithms from the plugin * @returns a dictionary with the name of the algorithm for sign and verify as the key @@ -52,3 +58,29 @@ export interface Signer { */ verify (signedContent: string, signature: string, jwk: PublicKey): Promise; } + +/** + * Interface for symmetric encryption and decryption + */ +export interface SymmetricEncrypter { + /** + * Given plaintext to encrypt, and additional authenticated data, creates corresponding ciphertext and + * provides the corresponding initialization vector, key, and tag. Note, not all + * @param plaintext Data to be symmetrically encrypted + * @param additionalAuthenticatedData Data that will be integrity checked but not encrypted + * @returns An object containing the corresponding ciphertext, initializationVector, key, and tag + */ + encrypt (plaintext: Buffer, additionalAuthenticatedData: Buffer): Promise<{ciphertext: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer}>; + + /** + * Given the ciphertext, additional authenticated data, initialization vector, key, and tag, + * decrypts the ciphertext. + * @param ciphertext Data to be decrypted + * @param additionalAuthenticatedData Integrity checked data + * @param initializationVector Initialization vector + * @param key Symmetric key + * @param tag Authentication tag + * @returns the plaintext of ciphertext + */ + decrypt (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer): Promise; +} diff --git a/lib/security/JoseToken.ts b/lib/security/JoseToken.ts index c2d328b..4b95875 100644 --- a/lib/security/JoseToken.ts +++ b/lib/security/JoseToken.ts @@ -1,4 +1,3 @@ -import Base64Url from '../utilities/Base64Url'; import CryptoFactory from '../CryptoFactory'; /** @@ -10,6 +9,19 @@ export default abstract class JoseToken { * Content of the token */ protected content: string; + + /** + * Protected headers (base64url encoded) + */ + protected protectedHeaders: string | undefined; + /** + * Unprotected headers + */ + protected unprotectedHeaders: {[member: string]: any} | undefined; + /** + * Payload (base64url encoded) + */ + protected payload: string | undefined; /** * Constructor for JoseToken that takes in a compact-serialized token string. */ @@ -24,13 +36,5 @@ export default abstract class JoseToken { /** * Gets the header as a JS object. */ - public getHeader (): any { - let [headerBase64Url] = this.content.split('.'); - if (!headerBase64Url) { - return; - } - const jsonString = Base64Url.decode(headerBase64Url); - - return JSON.parse(jsonString); - } + public abstract getHeader (): any; } diff --git a/lib/security/JweToken.ts b/lib/security/JweToken.ts index ed29d21..5663c74 100644 --- a/lib/security/JweToken.ts +++ b/lib/security/JweToken.ts @@ -1,8 +1,8 @@ -import * as crypto from 'crypto'; import Base64Url from '../utilities/Base64Url'; import JoseToken from './JoseToken'; import PublicKey from '../security/PublicKey'; import PrivateKey from '../security/PrivateKey'; +import CryptoFactory from '../CryptoFactory'; /** * Definition for a delegate that can encrypt data. @@ -14,53 +14,117 @@ type EncryptDelegate = (data: Buffer, jwk: PublicKey) => Promise; * This class hides the JOSE and crypto library dependencies to allow support for additional crypto algorithms. */ export default class JweToken extends JoseToken { + + // used for verification if a JSON Serialized JWS was given + private readonly encryptedKey: Buffer | undefined; + private readonly iv: Buffer | undefined; + private readonly tag: Buffer | undefined; + private readonly aad: Buffer | undefined; + + public constructor (content: string | object, protected cryptoFactory: CryptoFactory) { + super(content, cryptoFactory); + // check for compact JWE + if (typeof content === 'string') { + // 1. Parse JWE for components: BASE64URL(UTF8(JWE Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || + // BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || + // BASE64URL(JWE Authentication Tag) + const base64EncodedValues = content.split('.'); + if (base64EncodedValues.length === 5) { + // 2. Base64url decode the encoded header, encryption key, iv, ciphertext, and auth tag + this.protectedHeaders = base64EncodedValues[0]; + this.encryptedKey = Base64Url.decodeToBuffer(base64EncodedValues[1]); + this.iv = Base64Url.decodeToBuffer(base64EncodedValues[2]); + this.payload = base64EncodedValues[3]; + this.tag = Base64Url.decodeToBuffer(base64EncodedValues[4]); + // 15. Let the Additional Authentication Data (AAD) be ASCII(encodedprotectedHeader) + this.aad = Buffer.from(base64EncodedValues[0]); + return; + } + } + + const jsonContent: any = content; + if (typeof jsonContent === 'object' && + 'ciphertext' in jsonContent && typeof jsonContent.ciphertext === 'string' && + 'iv' in jsonContent && typeof jsonContent.iv === 'string' && + 'tag' in jsonContent && typeof jsonContent.tag === 'string' && + ('protected' in jsonContent || 'unprotected' in jsonContent || 'header' in jsonContent)) { + if ( + ('protected' in jsonContent && jsonContent.protected !== undefined && typeof jsonContent.protected !== 'string') || + ('unprotected' in jsonContent && jsonContent.unprotected !== undefined && typeof jsonContent.unprotected !== 'object') || + ('header' in jsonContent && jsonContent.header !== undefined && typeof jsonContent.header !== 'object') + ) { + // One of the properties is of the wrong type + return; + } + if ('recipients' in jsonContent) { + // TODO: General JWE JSON Serialization (Issue #22) + return; + } else if ('encrypted_key' in jsonContent && typeof jsonContent.encrypted_key === 'string') { + // Flattened JWE JSON Serialization + if ('header' in jsonContent) { + this.unprotectedHeaders = jsonContent.header; + } + this.encryptedKey = Base64Url.decodeToBuffer(jsonContent.encrypted_key); + } else { + // This isn't a JWE + return; + } + if ('protected' in jsonContent) { + this.protectedHeaders = jsonContent.protected; + } + if ('unprotected' in jsonContent) { + if (this.unprotectedHeaders) { + this.unprotectedHeaders = Object.assign(this.unprotectedHeaders, jsonContent.unprotected); + } else { + this.unprotectedHeaders = jsonContent.unprotected; + } + } + this.iv = Base64Url.decodeToBuffer(jsonContent.iv); + this.tag = Base64Url.decodeToBuffer(jsonContent.tag); + this.payload = jsonContent.ciphertext; + if (jsonContent.aad) { + this.aad = Buffer.from(this.protectedHeaders + '.' + jsonContent.aad); + } else { + this.aad = Buffer.from(this.protectedHeaders || ''); + } + } + } + /** - * Encrypts the given string in JWE compact serialized format using the given key in JWK JSON object format. - * Content encryption algorithm is hardcoded to 'A128GCM'. + * Encrypts the original content from construction into a JWE compact serialized format + * using the given key in JWK JSON object format.Content encryption algorithm is hardcoded to 'A128GCM'. * - * @returns Encrypted Buffer in JWE compact serialized format. + * @returns Buffer of the original content encrypted in JWE compact serialized format. */ public async encrypt (jwk: PublicKey, - additionalHeaders?: {[header: string]: string} - /* encryptionType?: string */): Promise { - /// TODO: extend to include encryptionType to determine symmetric key encryption using register + additionalHeaders?: {[header: string]: string}): Promise { // Decide key encryption algorithm based on given JWK. const keyEncryptionAlgorithm = jwk.defaultEncryptionAlgorithm; // Construct header. + const enc = this.cryptoFactory.getDefaultSymmetricEncryptionAlgorithm(); let header: {[header: string]: string} = Object.assign({}, { kid: jwk.kid, alg: keyEncryptionAlgorithm, - enc: 'A128GCM' + enc }, additionalHeaders); - // Base 64 encode header. + // Base64url encode header. const protectedHeaderBase64Url = Base64Url.encode(JSON.stringify(header)); - // Generate content encryption key. - const keyBuffer = crypto.randomBytes(16); + // Get the symmetric encrypter and encrypt + const symEncrypter = this.cryptoFactory.getSymmetricEncrypter(header.enc); + const symEnc = await symEncrypter.encrypt(Buffer.from(this.content), Buffer.from(protectedHeaderBase64Url)); // Encrypt content encryption key then base64-url encode it. - const encryptedKeyBuffer = await this.encryptContentEncryptionKey(keyEncryptionAlgorithm, keyBuffer, jwk); + const encryptedKeyBuffer = await this.encryptContentEncryptionKey(header.alg, symEnc.key, jwk); const encryptedKeyBase64Url = Base64Url.encode(encryptedKeyBuffer); - // Generate initialization vector then base64-url encode it. - const initializationVectorBuffer = crypto.randomBytes(12); - const initializationVectorBase64Url = Base64Url.encode(initializationVectorBuffer); - - // Encrypt content. - const cipher = crypto.createCipheriv('aes-128-gcm', keyBuffer, initializationVectorBuffer); - cipher.setAAD(Buffer.from(protectedHeaderBase64Url)); - const ciphertextBuffer = Buffer.concat([ - cipher.update(Buffer.from(this.content)), - cipher.final() - ]); - const ciphertextBase64Url = Base64Url.encode(ciphertextBuffer); - - // Get the authentication tag. - const authenticationTagBuffer = cipher.getAuthTag(); - const authenticationTagBase64Url = Base64Url.encode(authenticationTagBuffer); + // Get the base64s of the symmetric encryptions + const initializationVectorBase64Url = Base64Url.encode(symEnc.initializationVector); + const ciphertextBase64Url = Base64Url.encode(symEnc.ciphertext); + const authenticationTagBase64Url = Base64Url.encode(symEnc.tag); // Form final compact serialized JWE string. const jweString = [ @@ -74,6 +138,76 @@ export default class JweToken extends JoseToken { return Buffer.from(jweString); } + /** + * Encrypts the original content from construction into a JWE JSON serialized format using + * the given key in JWK JSON object format. Content encryption algorithm is hardcoded to 'A128GCM'. + * + * @returns Buffer of the original content encrytped in JWE flattened JSON serialized format. + */ + public async encryptFlatJson (jwk: PublicKey, + options?: { + unprotected?: {[key: string]: any}, + protected?: {[key: string]: any}, + aad?: string | Buffer + }): Promise<{ + protected?: string, + unprotected?: {[key: string]: string}, + encrypted_key: string, + iv: string, + ciphertext: string, + tag: string, + aad?: string + }> { + + // Decide key encryption algorithm based on given JWK. + const keyEncryptionAlgorithm = jwk.defaultEncryptionAlgorithm; + + // Construct header. + let header: {[header: string]: string} = Object.assign({}, { + kid: jwk.kid, + alg: keyEncryptionAlgorithm, + enc: this.cryptoFactory.getDefaultSymmetricEncryptionAlgorithm() + }, (options || {}).protected || {}); + + // Base64url encode header. + const protectedHeaderBase64Url = Base64Url.encode(JSON.stringify(header)); + + const aad = Buffer.from(options && options.aad ? `${protectedHeaderBase64Url}.${Base64Url.encode(options.aad)}` : protectedHeaderBase64Url); + + // Symmetrically encrypt the content + const symEncrypter = this.cryptoFactory.getSymmetricEncrypter(header.enc); + const symEncParams = await symEncrypter.encrypt(Buffer.from(this.content), aad); + + // Encrypt content encryption key and base64 all the parameters + const encryptedKeyBuffer = await this.encryptContentEncryptionKey(keyEncryptionAlgorithm, symEncParams.key, jwk); + const encryptedKeyBase64Url = Base64Url.encode(encryptedKeyBuffer); + const initializationVectorBase64Url = Base64Url.encode(symEncParams.initializationVector); + const ciphertextBase64Url = Base64Url.encode(symEncParams.ciphertext); + const authenticationTagBase64Url = Base64Url.encode(symEncParams.tag); + + // Form final compact serialized JWE string. + let returnJwe: { + protected?: string, + unprotected?: {[key: string]: string}, + encrypted_key: string, + iv: string, + ciphertext: string, + tag: string, + aad?: string + } = { + protected: protectedHeaderBase64Url, + unprotected: (options || {}).unprotected, + encrypted_key: encryptedKeyBase64Url, + iv: initializationVectorBase64Url, + ciphertext: ciphertextBase64Url, + tag: authenticationTagBase64Url + }; + if (options && options.aad) { + returnJwe.aad = Base64Url.encode(options.aad); + } + return returnJwe; + } + /** * Encrypts the given content encryption key using the specified algorithm and asymmetric public key. * @@ -98,32 +232,42 @@ export default class JweToken extends JoseToken { } /** - * Decrypts the given JWE compact serialized string using the given key in JWK JSON object format. - * TODO: implement decryption without node-jose dependency so we can use decryption algorithms from plugins. + * Gets the header as a JS object. + */ + public getHeader (): any { + let headers = this.unprotectedHeaders; + if (!headers) { + headers = {}; + } + if (this.protectedHeaders) { + const jsonString = Base64Url.decode(this.protectedHeaders); + const protect = JSON.parse(jsonString) as {[key: string]: any}; + headers = Object.assign(headers, protect); + } + return headers; + } + + /** + * Decrypts the original JWE using the given key in JWK JSON object format. * - * @returns Decrypted plaintext. + * @returns Decrypted plaintext of the JWE */ public async decrypt (jwk: PrivateKey): Promise { // following steps for JWE Decryption in RFC7516 section 5.2 - // 1. Parse JWE for components: BASE64URL(UTF8(JWE Header)) || '.' || BASE64URL(JWE Encrypted Key) || '.' || - // BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || - // BASE64URL(JWE Authentication Tag) - const base64EncodedValues = this.content.split('.'); - // 2. Base64url decode the encoded header, encryption key, iv, ciphertext, and auth tag - const headerString = Base64Url.decode(base64EncodedValues[0]); - const encryptedKey = Buffer.from(Base64Url.toBase64(base64EncodedValues[1]), 'base64'); - const iv = Buffer.from(Base64Url.toBase64(base64EncodedValues[2]), 'base64'); - const ciphertext = Buffer.from(Base64Url.toBase64(base64EncodedValues[3]), 'base64'); - const authTag = Buffer.from(Base64Url.toBase64(base64EncodedValues[4]), 'base64'); - // 3. let the JWE Header be a JSON object - const headers = JSON.parse(headerString); + if (this.encryptedKey === undefined || this.payload === undefined || this.iv === undefined || this.aad === undefined || this.tag === undefined) { + throw new Error('Could not parse contents into a JWE'); + } + const ciphertext = Base64Url.decodeToBuffer(this.payload); + + const headers = this.getHeader(); // 4. only applies to JWE JSON Serializaiton // 5. verify header fields - ['alg', 'enc', 'kid'].forEach((header: string) => { + ['alg', 'enc'].forEach((header: string) => { if (!(header in headers)) { throw new Error(`Missing required header: ${header}`); } }); + if ('crit' in headers) { // RFC7516 4.1.13/RFC7515 4.1.11 const extensions = headers.crit as string[]; if (extensions.filter) { @@ -140,12 +284,12 @@ export default class JweToken extends JoseToken { // 6. Determine the Key management mode by the "alg" header // TODO: Support other methods beyond key wrapping // 7. Verify that the JWE key is known - if (headers.kid !== jwk.kid) { + if (headers.kid && jwk.kid && headers.kid !== jwk.kid) { throw new Error('JWEToken key does not match provided jwk key'); } // 8. With keywrapping or direct key, let the jwk.kid be used to decrypt the encryptedkey // 9. Unwrap the encryptedkey to produce the content encryption key (CEK) - const cek = await (this.cryptoFactory.getEncrypter(headers.alg)).decrypt(encryptedKey, jwk); + const cek = await (this.cryptoFactory.getEncrypter(headers.alg)).decrypt(this.encryptedKey, jwk); // TODO: Verify CEK length meets "enc" algorithm's requirement // 10. TODO: Support direct key, then ensure encryptedKey === "" // 11. TODO: Support direct encryption, let CEK be the shared symmetric key @@ -153,31 +297,16 @@ export default class JweToken extends JoseToken { // 13. Skip due to JWE JSON Serialization format specific // 14. Compute the protected header: BASE64URL(UTF8(JWE Header)) // this would be base64Encodedvalues[0] - // 15. Let the Additional Authentication Data (AAD) be ASCII(encodedprotectedHeader) - const aad = base64EncodedValues[0]; // 16. Decrypt JWE Ciphertext using CEK, IV, AAD, and authTag, using "enc" algorithm. - // TODO: complex work involving symmetric key encryption here - const cryptoMap: {[enc: string]: string} = { - A128GCM: 'aes-128-gcm', - A192GCM: 'aes-192-gcm', - A256GCM: 'aes-256-gcm' - }; - const enc = cryptoMap[headers.enc]; - - const decipher = crypto.createDecipheriv(enc, cek, iv) as crypto.DecipherGCM; - decipher.setAAD(Buffer.from(aad, 'utf8')); - decipher.setAuthTag(authTag); - const plaintext = decipher.update(ciphertext, 'base64', 'utf8'); - if (decipher.final().length !== 0) { - throw new Error('crypto cipher final returned additional data'); - } + const symDecrypter = this.cryptoFactory.getSymmetricEncrypter(headers.enc); + const plaintext = await symDecrypter.decrypt(ciphertext, this.aad, this.iv, cek, this.tag); // 17. if a "zip" parameter was included, uncompress the plaintext using the specified algorithm if ('zip' in headers) { throw new Error('"zip" is not currently supported'); } // 18. If there was no recipient, the JWE is invalid. Otherwise output the plaintext. - return plaintext; + return plaintext.toString('utf8'); } } diff --git a/lib/security/JwsToken.ts b/lib/security/JwsToken.ts index 2eee713..74c6777 100644 --- a/lib/security/JwsToken.ts +++ b/lib/security/JwsToken.ts @@ -1,7 +1,7 @@ import Base64Url from '../utilities/Base64Url'; import JoseToken from './JoseToken'; import PublicKey from '../security/PublicKey'; -import { PrivateKey } from '..'; +import { PrivateKey, CryptoFactory } from '..'; /** * Definition for a delegate that can verfiy signed data. @@ -13,8 +13,51 @@ type VerifySignatureDelegate = (signedContent: string, signature: string, jwk: P * This class hides the JOSE and crypto library dependencies to allow support for additional crypto algorithms. */ export default class JwsToken extends JoseToken { + + // used for verification if a JSON Serialized JWS was given + private readonly signature: string | undefined; + + constructor (content: string | object, protected cryptoFactory: CryptoFactory) { + super(content, cryptoFactory); + // check for compact JWS + if (typeof content === 'string') { + const parts = content.split('.'); + if (parts.length === 3) { + this.protectedHeaders = parts[0]; + this.payload = parts[1]; + this.signature = parts[2]; + return; + } + } + // Check for JSON Serialization and reparse content if appropriate + if (typeof content === 'object') { + const jsonObject: any = content; + if ('payload' in jsonObject && typeof jsonObject.payload === 'string') { + // TODO: General JWS JSON Serialization signatures and one of protected or header for each (Issue #22) + if ('signature' in jsonObject && typeof jsonObject.signature === 'string') { + // Flattened JWS JSON Serialization + if (!('protected' in jsonObject && typeof jsonObject.protected === 'string') && + !('header' in jsonObject && typeof jsonObject.header === 'object')) { + // invalid JWS JSON Serialization + return; + } + // if we've gotten this far, we succeeded can can safely set parameters + if ('protected' in jsonObject && typeof jsonObject.protected === 'string') { + this.protectedHeaders = jsonObject.protected; + } + if ('header' in jsonObject && typeof jsonObject.header === 'object') { + this.unprotectedHeaders = jsonObject.header; + } + this.payload = jsonObject.payload; + this.signature = jsonObject.signature; + return; + } + } + } + } + /** - * Sign the given content using the given private key in JWK format. + * Signs contents given at construction using the given private key in JWK format. * * @param jwsHeaderParameters Header parameters in addition to 'alg' and 'kid' to be included in the JWS. * @returns Signed payload in compact JWS format. @@ -26,26 +69,65 @@ export default class JwsToken extends JoseToken { // 3. Compute the headers const headers = jwsHeaderParameters || {}; headers['alg'] = jwk.defaultSignAlgorithm; - headers['kid'] = jwk.kid; + if (jwk.kid) { + headers['kid'] = jwk.kid; + } // 4. Compute BASE64URL(UTF8(JWS Header)) const encodedHeaders = Base64Url.encode(JSON.stringify(headers)); // 5. Compute the signature using data ASCII(BASE64URL(UTF8(JWS Header))) || . || . BASE64URL(JWS Payload) // using the "alg" signature algorithm. const signatureInput = `${encodedHeaders}.${encodedContent}`; - const signature = await (this.cryptoFactory.getSigner(headers['alg'])).sign(signatureInput, jwk); + const signatureBase64 = await (this.cryptoFactory.getSigner(headers['alg'])).sign(signatureInput, jwk); // 6. Compute BASE64URL(JWS Signature) - const encodedSignature = Base64Url.fromBase64(signature); + const encodedSignature = Base64Url.fromBase64(signatureBase64); // 7. Only applies to JWS JSON Serializaiton // 8. Create the desired output: BASE64URL(UTF8(JWS Header)) || . BASE64URL(JWS payload) || . || BASE64URL(JWS Signature) return `${signatureInput}.${encodedSignature}`; } /** - * Verifies the given JWS compact serialized string using the given key in JWK object format. + * Signs contents given at construction using the given private key in JWK format with additional optional header fields + * @param jwk Private key used in the signature + * @param options Additional protected and header fields to include in the JWS + */ + public async signFlatJson (jwk: PrivateKey, + options?: {protected?: { [name: string]: string }, header?: { [name: string]: string }}): + Promise<{protected?: string, header?: {[name: string]: string}, payload: string, signature: string}> { + // Steps according to RTC7515 5.1 + // 2. Compute encoded payload vlaue base64URL(JWS Payload) + const encodedContent = Base64Url.encode(this.content); + // 3. Compute the headers + const header = (options || {}).header; + const protectedHeaders = (options || {}).protected || {}; + protectedHeaders['alg'] = jwk.defaultSignAlgorithm; + protectedHeaders['kid'] = jwk.kid; + // 4. Compute BASE64URL(UTF8(JWS Header)) + const encodedProtected = Base64Url.encode(JSON.stringify(protectedHeaders)); + // 5. Compute the signature using data ASCII(BASE64URL(UTF8(JWS Header))) || . || . BASE64URL(JWS Payload) + // using the "alg" signature algorithm. + const signatureInput = `${encodedProtected}.${encodedContent}`; + const signature = await (this.cryptoFactory.getSigner(protectedHeaders['alg'])).sign(signatureInput, jwk); + // 6. Compute BASE64URL(JWS Signature) + const encodedSignature = Base64Url.fromBase64(signature); + // 8. Create the desired output: BASE64URL(UTF8(JWS Header)) || . BASE64URL(JWS payload) || . || BASE64URL(JWS Signature) + return { + protected: encodedProtected, + header, + payload: encodedContent, + signature: encodedSignature + }; + } + + /** + * Verifies the JWS using the given key in JWK object format. * * @returns The payload if signature is verified. Throws exception otherwise. */ public async verifySignature (jwk: PublicKey): Promise { + // ensure we have everything we need + if (this.payload === undefined || this.signature === undefined) { + throw new Error('Could not parse contents into a JWS'); + } const algorithm = this.getHeader().alg; const signer = this.cryptoFactory.getSigner(algorithm); @@ -58,48 +140,42 @@ export default class JwsToken extends JoseToken { throw err; } - const signedContent = this.getSignedContent(); - const signature = this.getSignature(); - const passedSignatureValidation = await verify(signedContent, signature, jwk); + const signedContent = `${this.protectedHeaders || ''}.${this.payload}`; + const passedSignatureValidation = await verify(signedContent, this.signature, jwk); if (!passedSignatureValidation) { const err = new Error('Failed signature validation'); throw err; } - const verifiedData = this.getPayload(); + const verifiedData = Base64Url.decode(this.payload); return verifiedData; } - /** - * Gets the signed content (i.e. '
.'). - */ - private getSignedContent (): string { - const signedContentLength = this.content.lastIndexOf('.'); - const signedContent = this.content.substr(0, signedContentLength); - - return signedContent; - } - /** * Gets the base64 URL decrypted payload. */ public getPayload (): any { - const payloadStartIndex = this.content.indexOf('.') + 1; - const payloadExclusiveEndIndex = this.content.lastIndexOf('.'); - const payload = this.content.substring(payloadStartIndex, payloadExclusiveEndIndex); - - return Base64Url.decode(payload); + if (this.payload) { + return Base64Url.decode(this.payload); + } + return this.content; } /** - * Gets the signature string. + * Gets the header as a JS object. */ - private getSignature (): string { - const signatureStartIndex = this.content.lastIndexOf('.') + 1; - const signature = this.content.substr(signatureStartIndex); - - return signature; + public getHeader (): any { + let headers = this.unprotectedHeaders; + if (!headers) { + headers = {}; + } + if (this.protectedHeaders) { + const jsonString = Base64Url.decode(this.protectedHeaders); + const protect = JSON.parse(jsonString) as {[key: string]: any}; + headers = Object.assign(headers, protect); + } + return headers; } } diff --git a/lib/utilities/Base64Url.ts b/lib/utilities/Base64Url.ts index 55638fb..26e7afb 100644 --- a/lib/utilities/Base64Url.ts +++ b/lib/utilities/Base64Url.ts @@ -22,8 +22,15 @@ export default class Base64Url { * Decodes a Base64URL string. */ public static decode (base64urlString: string, encoding: string = 'utf8'): string { + return Base64Url.decodeToBuffer(base64urlString).toString(encoding); + } + + /** + * Decodes a Base64URL string + */ + public static decodeToBuffer (base64urlString: string): Buffer { const base64String = Base64Url.toBase64(base64urlString); - return Buffer.from(base64String, 'base64').toString(encoding); + return Buffer.from(base64String, 'base64'); } /** @@ -46,4 +53,5 @@ export default class Base64Url { .replace(/\//g, '_') .replace(/=/g, ''); } + } diff --git a/tests/Authentication.spec.ts b/tests/Authentication.spec.ts index 07d97a9..a61f8ef 100644 --- a/tests/Authentication.spec.ts +++ b/tests/Authentication.spec.ts @@ -1,5 +1,5 @@ import { DidDocument, unitTestExports } from '@decentralized-identity/did-common-typescript'; -import { Authentication, CryptoFactory, PublicKey, PrivateKey, JweToken, JwsToken, PrivateKeyRsa, RsaCryptoSuite } from '../lib'; +import { Authentication, CryptoFactory, PublicKey, PrivateKey, JweToken, JwsToken, PrivateKeyRsa, RsaCryptoSuite, AesCryptoSuite } from '../lib'; import VerifiedRequest from '../lib/interfaces/VerifiedRequest'; import AuthenticationResponse from '../lib/interfaces/AuthenticationResponse'; import AuthenticationRequest from '../lib/interfaces/AuthenticationRequest'; @@ -13,7 +13,7 @@ describe('Authentication', () => { let examplePublicKey: PublicKey; let exampleResolvedDID: DidDocument; let auth: Authentication; - let registry = new CryptoFactory([new RsaCryptoSuite()]); + let registry = new CryptoFactory([new RsaCryptoSuite(), new AesCryptoSuite()]); let resolver = new unitTestExports.TestResolver(); const hubDID = 'did:example:did'; const exampleDID = 'did:example:123456789abcdefghi'; diff --git a/tests/crypto/aes/AesCryptoSuite.spec.ts b/tests/crypto/aes/AesCryptoSuite.spec.ts new file mode 100644 index 0000000..69cc505 --- /dev/null +++ b/tests/crypto/aes/AesCryptoSuite.spec.ts @@ -0,0 +1,327 @@ +import AesCryptoSuite from '../../../lib/crypto/aes/AesCryptoSuite'; + +describe('AesCryptoSuite', () => { + let cryptosuite: AesCryptoSuite; + + beforeEach(() => { + cryptosuite = new AesCryptoSuite(); + }); + + describe('getEncrypters', () => { + it('should return nothing', () => { + expect(cryptosuite.getEncrypters()).toEqual({}); + }); + }); + + describe('getSigners', () => { + it('should return nothing', () => { + expect(cryptosuite.getSigners()).toEqual({}); + }); + }); + + describe('getKeyConstructors', () => { + it('should return nothing', () => { + expect(cryptosuite.getKeyConstructors()).toEqual({}); + }); + }); + + describe('getSymmetricEncrypters', () => { + let gcmEncryptSpy: jasmine.Spy; + let gcmDecryptSpy: jasmine.Spy; + let cbcEncryptSpy: jasmine.Spy; + let cbcDecryptSpy: jasmine.Spy; + + beforeEach(() => { + gcmEncryptSpy = spyOn(cryptosuite, 'encryptAesGcm' as any); + gcmDecryptSpy = spyOn(cryptosuite, 'decryptAesGcm' as any); + cbcEncryptSpy = spyOn(cryptosuite, 'encryptAesCbcHmacSha2' as any); + cbcDecryptSpy = spyOn(cryptosuite, 'decryptAesCbcHmacSha2' as any); + }); + + [128, 192, 256].forEach((keySize) => { + it(`should call encryptAesGcm and decryptAesGcm with ${keySize} for A${keySize}GCM`, () => { + cryptosuite.getSymmetricEncrypters(); + expect(gcmEncryptSpy).toHaveBeenCalledWith(keySize); + expect(gcmDecryptSpy).toHaveBeenCalledWith(keySize); + }); + + const hashSize = keySize * 2; + it(`should call encryptAesCbcHmacSha2 and decryptAesCbcHmacSha2 with + keySize ${keySize} and hashSize ${hashSize} for A${keySize}CBC-HS${hashSize}`, () => { + cryptosuite.getSymmetricEncrypters(); + expect(cbcEncryptSpy).toHaveBeenCalledWith(keySize, hashSize); + expect(cbcDecryptSpy).toHaveBeenCalledWith(keySize, hashSize); + }); + }); + }); + + describe('AES_CBC_HMAC_SHA2', () => { + describe('encrypt', () => { + it('should unit test AES_128_CBC_HMAC_SHA_256', async () => { + // rfc-751 B.1 + const K = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]; + // rfc-751 B.1 + const MAC_KEY = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]; + // rfc-751 B.1 + const ENC_KEY = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]; + // rfc-751 B.1 + const P = [0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, + 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, + 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, + 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65]; + // rfc-751 B.1 + const IV = [0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]; + // rfc-751 B.1 + const A = [0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, + 0x68, 0x6f, 0x66, 0x66, 0x73]; + // rfc-751 B.1 + const E = [0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, 0xa2, + 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, 0xa9, 0x4a, 0xc9, 0xb4, + 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, + 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, + 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, + 0xb9, 0xcf, 0x1b, 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, 0x4b, 0x88, 0x51, + 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb]; + // rfc-751 B.1 + const T = [0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4]; + // return set keys + let returnMacKey = true; + spyOn(cryptosuite, 'generateSymmetricKey' as any).and.callFake(() => { + if (returnMacKey) { + returnMacKey = false; + return Buffer.from(MAC_KEY); + } else { + return Buffer.from(ENC_KEY); + } + }); + spyOn(cryptosuite, 'generateInitializationVector' as any).and.returnValue(Buffer.from(IV)); + spyOn(cryptosuite, 'generateHmacTag' as any).and.returnValue(Buffer.from(T)); + const encrypters = cryptosuite.getSymmetricEncrypters(); + const params = await encrypters['A128CBC-HS256'].encrypt(Buffer.from(P), Buffer.from(A)); + expect(params.key).toEqual(Buffer.from(K)); + expect(params.initializationVector).toEqual((Buffer.from(IV))); + expect(params.ciphertext).toEqual(Buffer.from(E)); + expect(params.tag).toEqual(Buffer.from(T)); + }); + }); + describe('decrypt', () => { + it('should throw if the tag is incorrect', async () => { + // rfc-751 B.1 + const K = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]); + // rfc-751 B.1 + const IV = Buffer.from([0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]); + // rfc-751 B.1 + const A = Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, + 0x68, 0x6f, 0x66, 0x66, 0x73]); + // rfc-751 B.1 + const E = Buffer.from([0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, 0xa2, + 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, 0xa9, 0x4a, 0xc9, 0xb4, + 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, + 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, + 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, + 0xb9, 0xcf, 0x1b, 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, 0x4b, 0x88, 0x51, + 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb]); + // rfc-751 B.1 + const T = Buffer.from('I am not the correct tag'); + const encrypters = cryptosuite.getSymmetricEncrypters(); + try { + await encrypters['A128CBC-HS256'].decrypt(E, A, IV, K, T); + fail('should not have decrypted'); + } catch (err) { + expect(err.message).toContain('Invalid tag'); + } + }); + }); + + describe('validation', () => { + const values = [ + { + keySize: 128, + hashSize: 256, + // rfc-751 B.1 + K: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]), + MAC_KEY: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]), + ENC_KEY: Buffer.from([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]), + P: Buffer.from([0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, + 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, + 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, + 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65]), + IV: Buffer.from([0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]), + A: Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, + 0x68, 0x6f, 0x66, 0x66, 0x73]), + E: Buffer.from([0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, 0xa2, + 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, 0xa9, 0x4a, 0xc9, 0xb4, + 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, + 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, + 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, + 0xb9, 0xcf, 0x1b, 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, 0x4b, 0x88, 0x51, + 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb]), + T: Buffer.from([0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4]) + }, + { + keySize: 192, + hashSize: 384, + // rfc-751 B.2 + K: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f]), + MAC_KEY: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]), + ENC_KEY: Buffer.from([0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f]), + P: Buffer.from([0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65]), + IV: Buffer.from([0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]), + A: Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73]), + E: Buffer.from([0xea, 0x65, 0xda, 0x6b, 0x59, 0xe6, 0x1e, 0xdb, 0x41, 0x9b, 0xe6, 0x2d, 0x19, 0x71, 0x2a, 0xe5, + 0xd3, 0x03, 0xee, 0xb5, 0x00, 0x52, 0xd0, 0xdf, 0xd6, 0x69, 0x7f, 0x77, 0x22, 0x4c, 0x8e, 0xdb, + 0x00, 0x0d, 0x27, 0x9b, 0xdc, 0x14, 0xc1, 0x07, 0x26, 0x54, 0xbd, 0x30, 0x94, 0x42, 0x30, 0xc6, + 0x57, 0xbe, 0xd4, 0xca, 0x0c, 0x9f, 0x4a, 0x84, 0x66, 0xf2, 0x2b, 0x22, 0x6d, 0x17, 0x46, 0x21, + 0x4b, 0xf8, 0xcf, 0xc2, 0x40, 0x0a, 0xdd, 0x9f, 0x51, 0x26, 0xe4, 0x79, 0x66, 0x3f, 0xc9, 0x0b, + 0x3b, 0xed, 0x78, 0x7a, 0x2f, 0x0f, 0xfc, 0xbf, 0x39, 0x04, 0xbe, 0x2a, 0x64, 0x1d, 0x5c, 0x21, + 0x05, 0xbf, 0xe5, 0x91, 0xba, 0xe2, 0x3b, 0x1d, 0x74, 0x49, 0xe5, 0x32, 0xee, 0xf6, 0x0a, 0x9a, + 0xc8, 0xbb, 0x6c, 0x6b, 0x01, 0xd3, 0x5d, 0x49, 0x78, 0x7b, 0xcd, 0x57, 0xef, 0x48, 0x49, 0x27, + 0xf2, 0x80, 0xad, 0xc9, 0x1a, 0xc0, 0xc4, 0xe7, 0x9c, 0x7b, 0x11, 0xef, 0xc6, 0x00, 0x54, 0xe3]), + T: Buffer.from([0x84, 0x90, 0xac, 0x0e, 0x58, 0x94, 0x9b, 0xfe, 0x51, 0x87, 0x5d, 0x73, 0x3f, 0x93, 0xac, 0x20, + 0x75, 0x16, 0x80, 0x39, 0xcc, 0xc7, 0x33, 0xd7]) + }, + { + keySize: 256, + hashSize: 512, + // rfc-751 B.3 + K: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f]), + MAC_KEY: Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]), + ENC_KEY: Buffer.from([0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f]), + P: Buffer.from([0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, + 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, + 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, + 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, + 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, + 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, + 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65]), + IV: Buffer.from([0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]), + A: Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, + 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, + 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73]), + E: Buffer.from([0x4a, 0xff, 0xaa, 0xad, 0xb7, 0x8c, 0x31, 0xc5, 0xda, 0x4b, 0x1b, 0x59, 0x0d, 0x10, 0xff, 0xbd, + 0x3d, 0xd8, 0xd5, 0xd3, 0x02, 0x42, 0x35, 0x26, 0x91, 0x2d, 0xa0, 0x37, 0xec, 0xbc, 0xc7, 0xbd, + 0x82, 0x2c, 0x30, 0x1d, 0xd6, 0x7c, 0x37, 0x3b, 0xcc, 0xb5, 0x84, 0xad, 0x3e, 0x92, 0x79, 0xc2, + 0xe6, 0xd1, 0x2a, 0x13, 0x74, 0xb7, 0x7f, 0x07, 0x75, 0x53, 0xdf, 0x82, 0x94, 0x10, 0x44, 0x6b, + 0x36, 0xeb, 0xd9, 0x70, 0x66, 0x29, 0x6a, 0xe6, 0x42, 0x7e, 0xa7, 0x5c, 0x2e, 0x08, 0x46, 0xa1, + 0x1a, 0x09, 0xcc, 0xf5, 0x37, 0x0d, 0xc8, 0x0b, 0xfe, 0xcb, 0xad, 0x28, 0xc7, 0x3f, 0x09, 0xb3, + 0xa3, 0xb7, 0x5e, 0x66, 0x2a, 0x25, 0x94, 0x41, 0x0a, 0xe4, 0x96, 0xb2, 0xe2, 0xe6, 0x60, 0x9e, + 0x31, 0xe6, 0xe0, 0x2c, 0xc8, 0x37, 0xf0, 0x53, 0xd2, 0x1f, 0x37, 0xff, 0x4f, 0x51, 0x95, 0x0b, + 0xbe, 0x26, 0x38, 0xd0, 0x9d, 0xd7, 0xa4, 0x93, 0x09, 0x30, 0x80, 0x6d, 0x07, 0x03, 0xb1, 0xf6]), + T: Buffer.from([0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, + 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5]) + } + ]; + values.forEach((params) => { + it(`should match encrypt AES_${params.keySize}_CBC_HMAC_SHA_${params.hashSize}`, async () => { + // return set keys + let returnMacKey = true; + spyOn(cryptosuite, 'generateSymmetricKey' as any).and.callFake(() => { + if (returnMacKey) { + returnMacKey = false; + return Buffer.from(params.MAC_KEY); + } else { + return Buffer.from(params.ENC_KEY); + } + }); + spyOn(cryptosuite, 'generateInitializationVector' as any).and.returnValue(Buffer.from(params.IV)); + // No other returns are forced, this is a full validation + const encrypters = cryptosuite.getSymmetricEncrypters(); + const encryptReturns = await encrypters[`A${params.keySize}CBC-HS${params.hashSize}`].encrypt(params.P, params.A); + expect(encryptReturns.key).toEqual(params.K); + expect(encryptReturns.initializationVector).toEqual(params.IV); + expect(encryptReturns.ciphertext).toEqual(params.E); + expect(encryptReturns.tag).toEqual(params.T); + }); + + it(`should match decrypt AES_${params.keySize}_CBC_HMAC_SHA_${params.hashSize}`, async () => { + const encrypters = cryptosuite.getSymmetricEncrypters(); + const plaintext = await encrypters[`A${params.keySize}CBC-HS${params.hashSize}`].decrypt(params.E, params.A, params.IV, params.K, params.T); + expect(plaintext).toEqual(params.P); + }); + }); + }); + }); + + describe('generateHmacTag', () => { + it('should generate correct tag for AES_128_CBC_HMC_SHA_256 validation', async () => { + const keySize = 128; + const hashSize = 256; + const mac = Buffer.alloc(0); + const aad = Buffer.alloc(0); + const iv = Buffer.alloc(0); + const e = Buffer.alloc(0); + spyOn(cryptosuite, 'generateHmac' as any).and.returnValue(Buffer.from([0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, + 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4, 0xe6, 0xe5, 0x45, 0x82, 0x47, 0x65, 0x15, 0xf0, 0xad, 0x9f, 0x75, 0xa2, 0xb7, 0x1c, 0x73, 0xef])); + const tag = cryptosuite['generateHmacTag'](hashSize, keySize, mac, aad, iv, e); + expect(tag).toEqual(Buffer.from([0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4])); + }); + }); + + describe('generateHmac', () => { + it('should generate correct HMAC for AES_128_CBC_HMAC_SHA_256 validation', async () => { + const hashSize = 256; + const mac = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]); + const aad = Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, + 0x68, 0x6f, 0x66, 0x66, 0x73]); + const iv = Buffer.from([0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04]); + const e = Buffer.from([0xc8, 0x0e, 0xdf, 0xa3, 0x2d, 0xdf, 0x39, 0xd5, 0xef, 0x00, 0xc0, 0xb4, 0x68, 0x83, 0x42, 0x79, 0xa2, + 0xe4, 0x6a, 0x1b, 0x80, 0x49, 0xf7, 0x92, 0xf7, 0x6b, 0xfe, 0x54, 0xb9, 0x03, 0xa9, 0xc9, 0xa9, 0x4a, 0xc9, 0xb4, + 0x7a, 0xd2, 0x65, 0x5c, 0x5f, 0x10, 0xf9, 0xae, 0xf7, 0x14, 0x27, 0xe2, 0xfc, 0x6f, 0x9b, 0x3f, 0x39, 0x9a, 0x22, + 0x14, 0x89, 0xf1, 0x63, 0x62, 0xc7, 0x03, 0x23, 0x36, 0x09, 0xd4, 0x5a, 0xc6, 0x98, 0x64, 0xe3, 0x32, 0x1c, 0xf8, + 0x29, 0x35, 0xac, 0x40, 0x96, 0xc8, 0x6e, 0x13, 0x33, 0x14, 0xc5, 0x40, 0x19, 0xe8, 0xca, 0x79, 0x80, 0xdf, 0xa4, + 0xb9, 0xcf, 0x1b, 0x38, 0x4c, 0x48, 0x6f, 0x3a, 0x54, 0xc5, 0x10, 0x78, 0x15, 0x8e, 0xe5, 0xd7, 0x9d, 0xe5, 0x9f, + 0xbd, 0x34, 0xd8, 0x48, 0xb3, 0xd6, 0x95, 0x50, 0xa6, 0x76, 0x46, 0x34, 0x44, 0x27, 0xad, 0xe5, 0x4b, 0x88, 0x51, + 0xff, 0xb5, 0x98, 0xf7, 0xf8, 0x00, 0x74, 0xb9, 0x47, 0x3c, 0x82, 0xe2, 0xdb]); + const tag = cryptosuite['generateHmac'](hashSize, mac, aad, iv, e); + const M = Buffer.from([0x65, 0x2c, 0x3f, 0xa3, 0x6b, 0x0a, 0x7c, 0x5b, 0x32, 0x19, 0xfa, 0xb3, 0xa3, 0x0b, 0xc1, 0xc4, 0xe6, 0xe5, + 0x45, 0x82, 0x47, 0x65, 0x15, 0xf0, 0xad, 0x9f, 0x75, 0xa2, 0xb7, 0x1c, 0x73, 0xef]); + expect(tag).toEqual(M); + }); + }); + + describe('getAdditionalAuthenticatedDataLength', () => { + it('should generate correct AL for AES_128_CBC_HMAC_SHA_256 validation', async () => { + const aad = Buffer.from([0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, + 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, + 0x68, 0x6f, 0x66, 0x66, 0x73]); + const AL = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50]); + const al = cryptosuite['getAdditionalAuthenticatedDataLength'](aad); + expect(al).toEqual(AL); + }); + }); +}); diff --git a/tests/mocks/TestCryptoProvider.ts b/tests/mocks/TestCryptoProvider.ts index 0ee4ada..371b872 100644 --- a/tests/mocks/TestCryptoProvider.ts +++ b/tests/mocks/TestCryptoProvider.ts @@ -11,6 +11,8 @@ export default class TestCryptoSuite implements CryptoSuite { private static readonly DECRYPT = 0x2; private static readonly SIGN = 0x4; private static readonly VERIFY = 0x8; + private static readonly SYMENCRYPT = 0xF; + private static readonly SYMDECRYPT = 0x10; getKeyConstructors () { return { @@ -50,6 +52,27 @@ export default class TestCryptoSuite implements CryptoSuite { }; } + private symEncrypt (id: number): (plaintext: Buffer, _: Buffer) => + Promise<{ciphertext: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer}> { + return (plaintext: Buffer, _) => { + TestCryptoSuite.called[id] |= TestCryptoSuite.SYMENCRYPT; + return Promise.resolve({ + ciphertext: plaintext, + initializationVector: Buffer.alloc(0), + key: Buffer.alloc(0), + tag: Buffer.alloc(0) + }); + }; + } + + private symDecrypt (id: number): (ciphertext: Buffer, additionalAuthenticatedData: Buffer, initializationVector: Buffer, key: Buffer, tag: Buffer) => + Promise { + return (ciphertext: Buffer, _, __, ___, ____) => { + TestCryptoSuite.called[id] |= TestCryptoSuite.SYMDECRYPT; + return Promise.resolve(ciphertext); + }; + } + getEncrypters () { return { test: { @@ -68,6 +91,15 @@ export default class TestCryptoSuite implements CryptoSuite { }; } + getSymmetricEncrypters () { + return { + test: { + encrypt: this.symEncrypt(this.id), + decrypt: this.symDecrypt(this.id) + } + }; + } + /** * Returns true when encrypt() was called since last reset() */ @@ -96,6 +128,20 @@ export default class TestCryptoSuite implements CryptoSuite { return (TestCryptoSuite.called[this.id] & TestCryptoSuite.VERIFY) > 0; } + /** + * Returns true when Symmetric Encrypt was called since last reset() + */ + wasSymEncryptCalled (): boolean { + return (TestCryptoSuite.called[this.id] & TestCryptoSuite.SYMENCRYPT) > 0; + } + + /** + * Returns true when Symmetric Decrypt was called since last reset() + */ + wasSymDecryptCalled (): boolean { + return (TestCryptoSuite.called[this.id] & TestCryptoSuite.SYMDECRYPT) > 0; + } + /** * Resets visited flags for encrypt, decrypt, sign, and verify */ diff --git a/tests/security/JweToken.spec.ts b/tests/security/JweToken.spec.ts index aee9d18..fc72e67 100644 --- a/tests/security/JweToken.spec.ts +++ b/tests/security/JweToken.spec.ts @@ -1,14 +1,136 @@ import TestCryptoAlgorithms from '../mocks/TestCryptoProvider'; -import { PublicKey, JweToken, PrivateKey } from '../../lib'; +import { PublicKey, JweToken, PrivateKey, RsaCryptoSuite, AesCryptoSuite } from '../../lib'; import CryptoRegistry from '../../lib/CryptoFactory'; import TestPrivateKey from '../mocks/TestPrivateKey'; import Base64Url from '../../lib/utilities/Base64Url'; describe('JweToken', () => { + const crypto = new TestCryptoAlgorithms(); + let registry: CryptoRegistry; + + beforeEach(() => { + registry = new CryptoRegistry([crypto]); + }); + + describe('constructor', () => { + it('should construct from a flattened JSON object with a protected', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + encrypted_key: 'a key', + protected: 'secret properties' + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['protectedHeaders']).toEqual('secret properties'); + expect(jwe['payload']).toEqual('secrets'); + expect(jwe['unprotectedHeaders']).toBeUndefined(); + expect(jwe['iv']).toEqual(Base64Url.decodeToBuffer('vector')); + expect(jwe['tag']).toEqual(Base64Url.decodeToBuffer('tag')); + expect(jwe['encryptedKey']).toEqual(Base64Url.decodeToBuffer('a key')); + }); + it('should construct from a flattened JSON object with an unprotected', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + encrypted_key: 'a key', + unprotected: { + test: 'secret property' + } + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['unprotectedHeaders']).toBeDefined(); + expect(jwe['unprotectedHeaders']!['test']).toEqual('secret property'); + expect(jwe['payload']).toEqual('secrets'); + expect(jwe['iv']).toEqual(Base64Url.decodeToBuffer('vector')); + expect(jwe['tag']).toEqual(Base64Url.decodeToBuffer('tag')); + expect(jwe['encryptedKey']).toEqual(Base64Url.decodeToBuffer('a key')); + }); + it('should combine flattened JSON object headers unprotected and header', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + encrypted_key: 'a key', + unprotected: { + test: 'secret property' + }, + header: { + test2: 'secret boogaloo' + } + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['unprotectedHeaders']).toBeDefined(); + expect(jwe['unprotectedHeaders']!['test']).toEqual('secret property'); + expect(jwe['unprotectedHeaders']!['test2']).toEqual('secret boogaloo'); + }); + it('should accept flattened JSON object with only header', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + encrypted_key: 'a key', + header: { + test: 'secret boogaloo' + } + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['unprotectedHeaders']).toBeDefined(); + expect(jwe['unprotectedHeaders']!['test']).toEqual('secret boogaloo'); + }); + it('should require encrypted_key as a flattened JSON object', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + protected: 'secret properties' + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['protectedHeaders']).toBeUndefined(); + }); + it('should handle ignore general JSON serialization for now', () => { + const jweObject = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + protected: 'secret properties', + recipients: [] + }; + const jwe = new JweToken(jweObject, registry); + expect(jwe['protectedHeaders']).toBeUndefined(); + }); + + // test that it throws for incorrect types + ['protected', 'unprotected', 'header', 'encrypted_key', 'iv', 'tag', 'ciphertext'].forEach( + (property) => { + it(`should throw if ${property} is not the right type`, () => { + const jwe: any = { + ciphertext: 'secrets', + iv: 'vector', + tag: 'tag', + protected: 'secret properties', + unprotected: { + secrets: 'are everywhere' + }, + header: { + aliens: 'do you believe?' + } + }; + jwe[property] = true; + const token = new JweToken(jwe, registry); + expect(token['aad']).toBeUndefined(); + expect(token['encryptedKey']).toBeUndefined(); + expect(token['iv']).toBeUndefined(); + expect(token['payload']).toBeUndefined(); + expect(token['protectedHeaders']).toBeUndefined(); + expect(token['tag']).toBeUndefined(); + expect(token['unprotectedHeaders']).toBeUndefined(); + }); + }); + }); describe('encrypt', () => { - const crypto = new TestCryptoAlgorithms(); - let registry = new CryptoRegistry([crypto]); it('should fail for an unsupported encryption algorithm', () => { const testJwk = { @@ -61,9 +183,68 @@ describe('JweToken', () => { }); }); + describe('encryptFlatJson', () => { + it('should fail for an unsupported encryption algorithm', () => { + const testJwk = { + kty: 'RSA', + kid: 'did:example:123456789abcdefghi#keys-1', + defaultEncryptionAlgorithm: 'unknown', + defaultSignAlgorithm: 'test' + }; + const jwe = new JweToken('', registry); + jwe.encryptFlatJson(testJwk).then(() => { + fail('Error was not thrown.'); + }).catch( + (error) => { + expect(error).toMatch(/Unsupported encryption algorithm/i); + }); + }); + + it('should call the crypto Algorithms\'s encrypt', async () => { + crypto.reset(); + const jwk = { + kty: 'RSA', + kid: 'test', + defaultEncryptionAlgorithm: 'test', + defaultSignAlgorithm: 'test' + } as PublicKey; + const jwe = new JweToken('', registry); + await jwe.encryptFlatJson(jwk); + expect(crypto.wasEncryptCalled()).toBeTruthy(); + }); + + it('should accept additional options', async () => { + const jwk = { + kty: 'RSA', + kid: 'test', + defaultEncryptionAlgorithm: 'test', + defaultSignAlgorithm: 'test' + } as PublicKey; + const protectedValue = Math.round(Math.random()).toString(16); + const unprotectedValue = Math.round(Math.random()).toString(16); + const aad = Math.round(Math.random()).toString(16); + const plaintext = Math.round(Math.random()).toString(16); + const jwe = new JweToken(plaintext, registry); + crypto.reset(); + const encrypted = await jwe.encryptFlatJson(jwk, { + aad, + protected: { + test: protectedValue + }, + unprotected: { + test: unprotectedValue + } + }); + expect(crypto.wasEncryptCalled()).toBeTruthy(); + expect(encrypted).toBeDefined(); + expect(encrypted.aad).toEqual(Base64Url.encode(aad)); + expect(encrypted.unprotected!['test']).toEqual(unprotectedValue); + expect(JSON.parse(Base64Url.decode(encrypted.protected!))['test']).toEqual(protectedValue); + expect(encrypted.ciphertext).not.toEqual(plaintext); + }); + }); + describe('decrypt', () => { - const crypto = new TestCryptoAlgorithms(); - let registry = new CryptoRegistry([crypto]); let privateKey: PrivateKey; let plaintext: string; let encryptedMessage: string; @@ -99,7 +280,7 @@ describe('JweToken', () => { kty: 'test', kid: privateKey.kid, alg: 'unknown', - enc: 'A128GCM' + enc: 'test' }); const jwe = new JweToken(newMessage, registry); await expectToThrow(jwe, 'decrypt suceeded with unknown encryption algorithm used'); @@ -116,7 +297,7 @@ describe('JweToken', () => { const newMessage = usingheaders({ kty: 'test', kid: privateKey.kid, - enc: 'A128GCM' + enc: 'test' }); const jwe = new JweToken(newMessage, registry); await expectToThrow(jwe, 'decrypt succeeded when a necessary header was omitted'); @@ -126,7 +307,7 @@ describe('JweToken', () => { let message = usingheaders({ kty: 'test', kid: privateKey.kid, - enc: 'A128GCM', + enc: 'test', alg: 'test', test: 'A "required" field', crit: [ @@ -139,42 +320,280 @@ describe('JweToken', () => { message = usingheaders({ kty: 'test', kid: privateKey.kid, - enc: 'A128GCM', + enc: 'test', alg: 'test', test: 'A "required" field', crit: 1 }); jwe = new JweToken(message, registry); await expectToThrow(jwe, 'decrypt succeeded when a "crit" header was malformed', 'malformed'); - - message = usingheaders({ - kty: 'test', - kid: privateKey.kid, - enc: 'A128GCM', - alg: 'test', - test: 'A "required" field', - crit: [] - }); - jwe = new JweToken(message, registry); - await expectToThrow(jwe, 'decrypt decrypted data with mis-matched headers', 'authenticat'); // e or ion }); it('should require the key ids to match', async () => { const newMessage = usingheaders({ kty: 'test', kid: privateKey.kid + '1', - enc: 'A128GCM', + enc: 'test', alg: 'test' }); const jwe = new JweToken(newMessage, registry); await expectToThrow(jwe, 'decrypt succeeded when the private key does not match the headers key'); }); - it('should decrypt encrypted JWEs', async () => { + it('should decrypt compact JWEs', async () => { const jwe = new JweToken(encryptedMessage, registry); const payload = await jwe.decrypt(privateKey); expect(payload).toEqual(plaintext); }); + + it('should decrypt flattened JSON JWEs', async () => { + const compactComponents = encryptedMessage.split('.'); + const jwe = new JweToken({ + protected: compactComponents[0], + encrypted_key: compactComponents[1], + iv: compactComponents[2], + ciphertext: compactComponents[3], + tag: compactComponents[4] + }, registry); + const payload = await jwe.decrypt(privateKey); + expect(payload).toEqual(plaintext); + }); + + it('should decrypt flattened JSON JWEs using aad', async () => { + const pub = privateKey.getPublicKey(); + const aad = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16); + const jweToEncrypt = new JweToken(plaintext, registry); + const encrypted = await jweToEncrypt.encryptFlatJson(pub, { + aad + }); + expect(encrypted.aad).toEqual(Base64Url.encode(aad)); + const jwe = new JweToken(encrypted, registry); + const payload = await jwe.decrypt(privateKey); + expect(payload).toEqual(plaintext); + }); + + it('should require the JWE to have been parsed correctly', async () => { + const jwe = new JweToken('I am not decryptable', registry); + try { + await jwe.decrypt(privateKey); + fail('expected to throw'); + } catch (err) { + expect(err.message).toContain('Could not parse contents into a JWE'); + } + }); + }); + + describe('getHeader', () => { + it('should return headers from Compact JWE', () => { + const test = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jwe = new JweToken(protectedHeaders + '....', registry); + const headers = jwe.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + }); + + it('should return headers from Flattened JSON Serialization', () => { + const test = Math.random().toString(16); + const headertest = Math.random().toString(16); + const unprotectedtest = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jwe = new JweToken({ + protected: protectedHeaders, + header: { + headertest + }, + unprotected: { + unprotectedtest + }, + ciphertext: '', + iv: '', + tag: '', + encrypted_key: '' + }, registry); + const headers = jwe.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + expect(headers['headertest']).toEqual(headertest); + expect(headers['unprotectedtest']).toEqual(unprotectedtest); + }); + + it('should return headers from Flattened JSON Serialization with only header', () => { + const headertest = Math.random().toString(16); + const unprotectedtest = Math.random().toString(16); + const jwe = new JweToken({ + header: { + headertest + }, + unprotected: { + unprotectedtest + }, + ciphertext: '', + iv: '', + tag: '', + encrypted_key: '' + }, registry); + const headers = jwe.getHeader(); + expect(headers).toBeDefined(); + expect(headers['headertest']).toEqual(headertest); + expect(headers['unprotectedtest']).toEqual(unprotectedtest); + }); + + it('should return headers from Flattened JSON Serialization with only protected', () => { + const test = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jwe = new JweToken({ + protected: protectedHeaders, + ciphertext: '', + iv: '', + tag: '', + encrypted_key: '' + }, registry); + const headers = jwe.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + }); + }); + + describe('validations', () => { + describe('RSAES-OAEP with AES GCM', () => { + let aes: AesCryptoSuite; + // needs the actual RSA and AES implementations + beforeEach(() => { + aes = new AesCryptoSuite(); + registry = new CryptoRegistry([new RsaCryptoSuite(), aes]); + }); + + // rfc-7516 A.1 + const plaintext = Buffer.from([84, 104, 101, 32, 116, 114, 117, 101, 32, 115, 105, 103, 110, 32, + 111, 102, 32, 105, 110, 116, 101, 108, 108, 105, 103, 101, 110, 99, + 101, 32, 105, 115, 32, 110, 111, 116, 32, 107, 110, 111, 119, 108, + 101, 100, 103, 101, 32, 98, 117, 116, 32, 105, 109, 97, 103, 105, + 110, 97, 116, 105, 111, 110, 46]); + // rfc-7516 A.1.1 + const expectedProtectedHeader = { alg: 'RSA-OAEP',enc: 'A256GCM' }; + // rfc-7516 A.1.1 + const encodedProtectedHeader = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ'; + // rfc-7516 A.1.2 + const cek = [177, 161, 244, 128, 84, 143, 225, 115, 63, 180, 3, 255, 107, 154, + 212, 246, 138, 7, 110, 91, 112, 46, 34, 105, 47, 130, 203, 46, 122, + 234, 64, 252]; + // rfc-7516 A.1.3 + const rsaKey = {kty: 'RSA', + n: 'oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUWcJoZmds2h7M70imEVhRU5djINXtqllXI4D' + + 'FqcI1DgjT9LewND8MW2Krf3Spsk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2asbOenSZeyaxzi' + + 'K72UwxrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMStPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsN' + + 'a7LZK2djYgyD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw', + e: 'AQAB', + d: 'kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5NWV5KntaEeXS1j82E375xxhWMHXyvjYecPT' + + '9fpwR_M9gV8n9Hrh2anTpTD93Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghkqDp0Vqj3kbSCz' + + '1XyfCs6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vlt3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-' + + 'VFs3VSndVTIznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ', + p: '1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAE' + + 'PTPjXv_hI2_1eTSPVZfzL0lffNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0', + q: 'wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBmUDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28' + + 'cRUoeBB_jKI1oma0Orv1T9aXIWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc', + dp: 'ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KLhMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTj' + + 'Vw_x4NyqyXPM5nULPkcpU827rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE', + dq: 'Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCjywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk' + + '8g6Qn29Tt0cj8qqyFpz9vNDBUfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis', + qi: 'VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOy' + + 'dB61gwanOsXGOAOv82cHq0E3eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY' + }; + // rfc-7516 A.1.3 + const cekEncrypted = [56, 163, 154, 192, 58, 53, 222, 4, 105, 218, 136, 218, 29, 94, 203, + 22, 150, 92, 129, 94, 211, 232, 53, 89, 41, 60, 138, 56, 196, 216, + 82, 98, 168, 76, 37, 73, 70, 7, 36, 8, 191, 100, 136, 196, 244, 220, + 145, 158, 138, 155, 4, 117, 141, 230, 199, 247, 173, 45, 182, 214, + 74, 177, 107, 211, 153, 11, 205, 196, 171, 226, 162, 128, 171, 182, + 13, 237, 239, 99, 193, 4, 91, 219, 121, 223, 107, 167, 61, 119, 228, + 173, 156, 137, 134, 200, 80, 219, 74, 253, 56, 185, 91, 177, 34, 158, + 89, 154, 205, 96, 55, 18, 138, 43, 96, 218, 215, 128, 124, 75, 138, + 243, 85, 25, 109, 117, 140, 26, 155, 249, 67, 167, 149, 231, 100, 6, + 41, 65, 214, 251, 232, 87, 72, 40, 182, 149, 154, 168, 31, 193, 126, + 215, 89, 28, 111, 219, 125, 182, 139, 235, 195, 197, 23, 234, 55, 58, + 63, 180, 68, 202, 206, 149, 75, 205, 248, 176, 67, 39, 178, 60, 98, + 193, 32, 238, 122, 96, 158, 222, 57, 183, 111, 210, 55, 188, 215, + 206, 180, 166, 150, 166, 106, 250, 55, 229, 72, 40, 69, 214, 216, + 104, 23, 40, 135, 212, 28, 127, 41, 80, 175, 174, 168, 115, 171, 197, + 89, 116, 92, 103, 246, 83, 216, 182, 176, 84, 37, 147, 35, 45, 219, + 172, 99, 226, 233, 73, 37, 124, 42, 72, 49, 242, 35, 127, 184, 134, + 117, 114, 135, 206]; + // rfc-7516 A.1.4 + const iv = [227, 197, 117, 252, 2, 219, 233, 68, 180, 225, 77, 219]; + // rfc-7516 A.1.5 + const aad = [101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, + 116, 84, 48, 70, 70, 85, 67, 73, 115, 73, 109, 86, 117, 89, 121, 73, + 54, 73, 107, 69, 121, 78, 84, 90, 72, 81, 48, 48, 105, 102, 81]; + // rfc-7516 A.1.6 + const ciphertext = [229, 236, 166, 241, 53, 191, 115, 196, 174, 43, 73, 109, 39, 122, + 233, 96, 140, 206, 120, 52, 51, 237, 48, 11, 190, 219, 186, 80, 111, + 104, 50, 142, 47, 167, 59, 61, 181, 127, 196, 21, 40, 82, 242, 32, + 123, 143, 168, 226, 73, 216, 176, 144, 138, 247, 106, 60, 16, 205, + 160, 109, 64, 63, 192]; + // rfc-7516 A.1.6 + const tag = [92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, + 210, 145]; + // rfc-7516 A.1.7 + const JWE = 'eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.' + + 'OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGe' + + 'ipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDb' + + 'Sv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaV' + + 'mqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je8' + + '1860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi' + + '6UklfCpIMfIjf7iGdXKHzg.' + + '48V1_ALb6US04U3b.' + + '5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6ji' + + 'SdiwkIr3ajwQzaBtQD_A.' + + 'XFBoMYUZodetZdvTiFvSkQ'; + it('should parse the compact JWE correctly', () => { + const parsedJwe = new JweToken(JWE, registry); + expect(parsedJwe['aad']).toEqual(Buffer.from(aad)); + expect(parsedJwe['encryptedKey']).toEqual(Buffer.from(cekEncrypted)); + expect(parsedJwe['iv']).toEqual(Buffer.from(iv)); + expect(parsedJwe['payload']).toEqual(Base64Url.encode(Buffer.from(ciphertext))); + expect(parsedJwe['protectedHeaders']).toEqual(encodedProtectedHeader); + expect(parsedJwe['tag']).toEqual(Buffer.from(tag)); + expect(parsedJwe['unprotectedHeaders']).toBeUndefined(); + }); + + it('should decrypt correctly', async () => { + const parsedJwe = new JweToken(JWE, registry); + const actualPlaintext = await parsedJwe.decrypt(rsaKey as any); + expect(actualPlaintext).toEqual(plaintext.toString()); + }); + + it('should encrypt correctly', async (done) => { + // set AES to return the expected IV and CEK + spyOn(aes, 'generateInitializationVector' as any).and.returnValue(Buffer.from(iv)); + aes['generateSymmetricKey'] = (_: number) => { return Buffer.from(cek); }; + setTimeout(async () => { + const plaintextString = plaintext.toString(); + const jwe = new JweToken(plaintextString, registry); + + const publicKey = { + kty: 'RSA', + n: rsaKey.n, + e: rsaKey.e + }; + const encrypted = await jwe.encrypt(publicKey as any, expectedProtectedHeader); + // rfc-7516 A.1.8 CEK cannot be validated however other parameters should match. + const actual = encrypted.toString().split('.'); + const expected = JWE.split('.'); + expect(actual[0]).toEqual(expected[0]); + expect(actual[2]).toEqual(expected[2]); + expect(actual[3]).toEqual(expected[3]); + expect(actual[4]).toEqual(expected[4]); + done(); + }, 100); + }); + }); }); }); diff --git a/tests/security/JwsToken.spec.ts b/tests/security/JwsToken.spec.ts index 5e31704..6a7faad 100644 --- a/tests/security/JwsToken.spec.ts +++ b/tests/security/JwsToken.spec.ts @@ -4,16 +4,94 @@ import Base64Url from '../../lib/utilities/Base64Url'; import { TestPublicKey } from '../mocks/TestPublicKey'; import CryptoFactory from '../../lib/CryptoFactory'; import TestPrivateKey from '../mocks/TestPrivateKey'; +import { RsaCryptoSuite } from '../../lib'; describe('JwsToken', () => { + const crypto = new TestCryptoAlgorithms(); + let registry: CryptoFactory; + beforeEach(() => { + registry = new CryptoFactory([crypto]); + }); - describe('verifySignature', () => { - const crypto = new TestCryptoAlgorithms(); - let registry: CryptoFactory; - beforeEach(() => { - registry = new CryptoFactory([crypto]); + describe('constructor', () => { + it('should construct from a flattened JSON object', () => { + const correctJWS = { + protected: 'foo', + payload: 'foobar', + signature: 'baz' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toEqual('foo'); + expect(jws['payload']).toEqual('foobar'); + expect(jws['signature']).toEqual('baz'); + expect(jws['unprotectedHeaders']).toBeUndefined(); + }); + + it('should construct from a flattened JSON object using header', () => { + const correctJWS = { + header: { + alg: 'test', + kid: 'test' + }, + payload: 'foobar', + signature: 'baz' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toBeUndefined(); + expect(jws['unprotectedHeaders']).toBeDefined(); + expect(jws['unprotectedHeaders']!['kid']).toEqual('test'); + expect(jws['payload']).toEqual('foobar'); + expect(jws['signature']).toEqual('baz'); + }); + + it('should include nonprotected headers', () => { + const correctJWS = { + protected: 'foo', + header: { + foo: 'bar' + }, + payload: 'foobar', + signature: 'baz' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toEqual('foo'); + expect(jws['payload']).toEqual('foobar'); + expect(jws['signature']).toEqual('baz'); + expect(jws['unprotectedHeaders']).toBeDefined(); + expect(jws['unprotectedHeaders']!['foo']).toEqual('bar'); }); + it('should ignore objects with invalid header formats', () => { + const correctJWS = { + header: 'wrong', + payload: 'foobar', + signature: 'baz' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toBeUndefined(); + }); + + it('should ignore objects missing protected and header', () => { + const correctJWS = { + payload: 'foobar', + signature: 'baz' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toBeUndefined(); + }); + + it('should ignore objects missing signature', () => { + const correctJWS = { + protected: 'foo', + payload: 'foobar' + }; + const jws = new JwsToken(correctJWS, registry); + expect(jws['protectedHeaders']).toBeUndefined(); + }); + }); + + describe('verifySignature', () => { + const header = { alg: 'test', kid: 'did:example:123456789abcdefghi#keys-1' @@ -72,12 +150,114 @@ describe('JwsToken', () => { } expect(crypto.wasVerifyCalled()).toBeTruthy(); }); + + it('should require the JWS to have been parsed correctly', async () => { + const jws = new JwsToken('I am not decryptable', registry); + try { + await jws.verifySignature(new TestPublicKey()); + fail('expected to throw'); + } catch (err) { + expect(err.message).toContain('Could not parse contents into a JWS'); + } + }); }); - describe('sign', () => { - const crypto = new TestCryptoAlgorithms(); - let registry = new CryptoFactory([crypto]); + describe('getHeader', () => { + it('should return headers from Compact JWS', () => { + const test = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jws = new JwsToken(protectedHeaders + '..', registry); + const headers = jws.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + }); + + it('should return headers from Flattened JSON Serialization', () => { + const test = Math.random().toString(16); + const headertest = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jws = new JwsToken({ + protected: protectedHeaders, + header: { + headertest + }, + payload: '', + signature: '' + }, registry); + const headers = jws.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + expect(headers['headertest']).toEqual(headertest); + }); + + it('should return headers from Flattened JSON Serialization with only header', () => { + const headertest = Math.random().toString(16); + const jws = new JwsToken({ + header: { + headertest + }, + payload: '', + signature: '' + }, registry); + const headers = jws.getHeader(); + expect(headers).toBeDefined(); + expect(headers['headertest']).toEqual(headertest); + }); + + it('should return headers from Flattened JSON Serialization with only protected', () => { + const test = Math.random().toString(16); + const protectedHeaders = Base64Url.encode(JSON.stringify({ + test + })); + const jws = new JwsToken({ + protected: protectedHeaders, + payload: '', + signature: '' + }, registry); + const headers = jws.getHeader(); + expect(headers).toBeDefined(); + expect(headers['test']).toEqual(test); + }); + }); + + describe('getPayload', () => { + let data: string; + let payload: string; + + beforeEach(() => { + data = JSON.stringify({ + test: Math.random() + }); + payload = Base64Url.encode(data); + }); + + it('should return the payload from a compact JWS', () => { + const jws = new JwsToken(`.${payload}.`, registry); + expect(jws.getPayload()).toEqual(data); + }); + + it('should return the payload from a Flattened JSON JWS', () => { + const jws = new JwsToken({ + header: { + alg: 'none' + }, + payload, + signature: '' + }, registry); + expect(jws.getPayload()).toEqual(data); + }); + + it('should return the original content if it was unable to parse a JWS', () => { + const jws = new JwsToken('some test value', registry); + expect(jws.getPayload()).toEqual('some test value'); + }); + }); + describe('sign', () => { const data = { description: 'JWSToken test' }; @@ -102,4 +282,132 @@ describe('JwsToken', () => { expect(crypto.wasSignCalled()).toBeTruthy(); }); }); + + describe('signFlatJson', () => { + + let data: any; + + beforeEach(() => { + data = { + description: `test: ${Math.random()}` + }; + }); + + it('should throw an error because the algorithm is not supported', async () => { + const privateKey = new TestPrivateKey(); + privateKey.defaultSignAlgorithm = 'unsupported'; + const jwsToken = new JwsToken(data, registry); + try { + await jwsToken.signFlatJson(privateKey); + } catch (err) { + expect(err).toBeDefined(); + return; + } + fail('Sign did not throw'); + }); + + it('should call the crypto Algorithms\'s sign', async () => { + const jwsToken = new JwsToken(data, registry); + crypto.reset(); + await jwsToken.signFlatJson(new TestPrivateKey()); + expect(crypto.wasSignCalled()).toBeTruthy(); + }); + + it('should return the expected JSON JWS', async () => { + const jwsToken = new JwsToken(data, registry); + const key = new TestPrivateKey(); + const jws = await jwsToken.signFlatJson(key); + expect(jws.signature).toBeDefined(); + expect(Base64Url.decode(jws.payload)).toEqual(JSON.stringify(data)); + }); + }); + + describe('validations', () => { + + beforeEach(() => { + registry = new CryptoFactory([new RsaCryptoSuite()]); + }); + + describe('RSASSA-PKCS1-v1_5 SHA-256', () => { + // rfc-7515 A.2.1 + const headers = { alg: 'RS256' }; + // rfc-7515 A.2.1 + const encodedHeaders = 'eyJhbGciOiJSUzI1NiJ9'; + // rfc-7515 A.2.1 + const payload = Buffer.from([123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, + 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, + 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, + 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, + 111, 116, 34, 58, 116, 114, 117, 101, 125]); + // rfc-7515 A.2.1 + const encodedPayload = 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' + + 'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ'; + // rfc-7515 A.2.1 + const rsaKey = { + kty: 'RSA', + n: 'ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddx' + + 'HmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMs' + + 'D1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSH' + + 'SXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdV' + + 'MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8' + + 'NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ', + e: 'AQAB', + d: 'Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97I' + + 'jlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0' + + 'BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn' + + '439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYT' + + 'CBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLh' + + 'BOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ', + p: '4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdi' + + 'YrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPG' + + 'BY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc', + q: 'uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxa' + + 'ewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA' + + '-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc', + dp: 'BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3Q' + + 'CLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb' + + '34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0', + dq: 'h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa' + + '7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-ky' + + 'NlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU', + qi: 'IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2o' + + 'y26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLU' + + 'W0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U' + }; + // rfc-7515 A.2.1 + const finalJws = 'eyJhbGciOiJSUzI1NiJ9' + + '.' + + 'eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt' + + 'cGxlLmNvbS9pc19yb290Ijp0cnVlfQ' + + '.' + + 'cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7' + + 'AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4' + + 'BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K' + + '0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqv' + + 'hJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrB' + + 'p0igcN_IoypGlUPQGe77Rw'; + + it('signs correctly', async () => { + const jws = new JwsToken(payload.toString(), registry); + const privateKey: any = rsaKey; + privateKey['defaultSignAlgorithm'] = 'RS256'; + const signed = await jws.sign(privateKey); + expect(signed).toEqual(finalJws); + }); + + it('should validate correctly', async () => { + const jws = new JwsToken(finalJws, registry); + expect(jws['protectedHeaders']).toEqual(encodedHeaders); + expect(jws['payload']).toEqual(encodedPayload); + expect(jws.getHeader()).toEqual(headers); + const publicKey: any = { + kty: 'RSA', + n: rsaKey.n, + e: rsaKey.e + }; + const actualPayload = await jws.verifySignature(publicKey); + expect(actualPayload).toEqual(payload.toString()); + }); + }); + }); });