From d96812074bb34e73296d2f7c2d29740e73409a3f Mon Sep 17 00:00:00 2001 From: Oliver Manzi Date: Wed, 6 Sep 2023 14:15:29 +0200 Subject: [PATCH] (v3.0.0) breaking: corrects poor URI design decision in v2.x.x - removes quotation marks from SSASy URIs - adds config option to deserialize key as raw key - adds support for legacy resources - adds an end-to-end testing --- .eslintrc | 4 + docs/changelog.md | 9 + package.json | 2 +- src/modules/serializer-mod.ts | 545 ++++++++++++++++++++------- src/wallet.ts | 163 ++++---- tests/index.test.ts | 171 +++++++++ tests/modules/serializer-mod.test.ts | 271 ++++++------- tests/wallet.test.ts | 72 ++-- 8 files changed, 796 insertions(+), 441 deletions(-) create mode 100644 tests/index.test.ts diff --git a/.eslintrc b/.eslintrc index f2f643f..0f580cf 100644 --- a/.eslintrc +++ b/.eslintrc @@ -34,6 +34,10 @@ } } ], + // no skipping of tests + "mocha/no-skipped-tests": ["error"], + // no exclusive tests + "mocha/no-exclusive-tests": ["error"], /* ================ formatting ================ */ // use 2 spaces for indentation diff --git a/docs/changelog.md b/docs/changelog.md index 9f3c0c2..9885f06 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,15 @@ > Only notable changes are documented here. +## `3.0.0` - Fixes SSASy URI format + +- `3.0.0` is essentially a culmination of `2.0.0` and `2.2.0` with some minor changes to the SSASy URI format. +- [breaking] removes quotes from SSASy URI format to avoid issues with URI encoding caused by the quotes. This is a breaking change because it changes the format of the SSASy URI which may cause issues for users who have already stored SSASy URIs in a database, for example. + +### Migrating from `2.2.x` to `3.0.0` + +- Convert all SSASy URIs back to `RawKey`s using v`2.2.x` and then convert them to SSASy URIs using v`3.0.0`. + ## `2.2.2` - SSASy Key URI - [patch] removes `raw` param from SSASy key URI. The `raw` param was used to indicate whether the key should be deserialized to a `RawKey` object or a `SecureContextKey` object. Since some keys may have the `raw` param while others do not, it can cause issues when searching for keys in a database, for example, since the `raw` param is not part of the key's URI. To solve this minor design issue, the `raw` param has been removed. diff --git a/package.json b/package.json index b005257..c4a8e6a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ssasy-auth/core", "license": "MIT", - "version": "2.2.3", + "version": "3.0.0", "description": "a self-sovereign authentication scheme", "author": "hello@oliverrr.net", "repository": "ssasy-auth/core", diff --git a/src/modules/serializer-mod.ts b/src/modules/serializer-mod.ts index d310926..b593b3f 100644 --- a/src/modules/serializer-mod.ts +++ b/src/modules/serializer-mod.ts @@ -17,25 +17,34 @@ import type { PublicKey, RawKey, GenericKey, + SecureContextKey, Challenge, Ciphertext, StandardCiphertext, AdvancedCiphertext } from "../interfaces"; -export const SERIALIZER_ERROR_MESSAGE = { +const SERIALIZER_ERROR_MESSAGE = { INVALID_KEY: "Key is invalid or not supported", INVALID_KEY_STRING: "Key is invalid", + INVALID_CHALLENGE: "Challenge is invalid", INVALID_CHALLENGE_STRING: "Challenge string is invalid", + INVALID_CIPHERTEXT: "Ciphertext is invalid", INVALID_CIPHERTEXT_STRING: "Ciphertext string is invalid", + INVALID_SIGNATURE: "Signature is invalid", INVALID_SIGNATURE_STRING: "Signature string is invalid", + + MISSING_PARAM: "Parameter key or value is missing", MISSING_KEY_STRING: "Key is missing", MISSING_CHALLENGE_STRING: "Challenge string is missing", MISSING_CIPHERTEXT_STRING: "Ciphertext string is missing", - MISSING_SIGNATURE_STRING: "Signature string is missing" + MISSING_SIGNATURE_STRING: "Signature string is missing", + + LEGACY_INVALID_CIPHERTEXT_STRING: "Legacy ciphertext string is invalid", + LEGACY_INVALID_CHALLENGE_STRING: "Legacy challenge string is invalid" }; /** @@ -45,7 +54,6 @@ export const SERIALIZER_ERROR_MESSAGE = { function _encodeUriParamValue(value: string): string { return encodeURIComponent(value) .replace(/&/g, "%26") - .replace(/,/g, "%2C") .replace(/=/g, "%3D") .replace(/'/g, "%27") .replace(/"/g, "%22") @@ -58,12 +66,154 @@ function _encodeUriParamValue(value: string): string { function _decodeUriParam(value: string): string { return decodeURIComponent(value) .replace(/%26/g, "&") - .replace(/%2C/g, ",") .replace(/%3D/g, "=") .replace(/%27/g, "'") .replace(/%22/g, "\"") } +/** + * Returns a uri parameter from a key-value pair. The parameter is prefixed + * with an ampersand (`&`) if it is not the first parameter in the uri and + * the value is encoded. + * + * @param key - parameter key + * @param value - parameter value + * @param config - configuration object + * @param config.first - indicates whether the parameter is the first parameter in the uri + */ +function _constructParam(key: string, value: string, config?: { first?: boolean}): string { + + if(!key || !value){ + throw new Error(SERIALIZER_ERROR_MESSAGE.MISSING_PARAM) + } + + // encode value + value = _encodeUriParamValue(value); + + return config?.first ? `${key}=${value}` : `&${key}=${value}`; +} + +/** + * Returns key and value from a uri parameter. + * + * @param param - uri parameter + * @returns key and value + */ +function _deconstructParam(param: string): { key: string, value: string } { + // get first index of "=" to split property into key and value + // note: this is a workaround for edge-cases where a value contains "=" (i.e. iv="gjhgdfhshgadhfga==") + const equalOperatorIndex = param.indexOf("="); + + let key: string = param.slice(0, equalOperatorIndex); + let value: string = param.slice(equalOperatorIndex + 1); + + // remove ampersand from key (e.g. &key=value) + if(key.startsWith("&")){ + key = key.slice(1); + } + + // decode property value + value = _decodeUriParam(value); + + return { key, value }; +} + +/** + * ! Temporary fix for legacy resource strings + */ +const LegacySerializerModule = { + /** + * Convert legacy key string back to a key object. + * Old key string format: JSON.stringify(rawKey) + */ + deserializeKey: (legacyKeyUri: string): RawKey => { + return JSON.parse(legacyKeyUri); + }, + /** + * Converts legacy ciphertext string back to a ciphertext object. + * Old ciphertext string format: `"{ iv, data, salt?, sender?, recipient?, signature? }"` + * + * @param ciphertextUri - legacy ciphertext string + * @returns ciphertext object + * + */ + deserializeCiphertext: async (legacyCiphertextUri: string): Promise => { + interface ShallowCiphertext extends Omit { + sender?: string; + recipient?: string; + signature?: string; + } + + const shallowCiphertext: ShallowCiphertext = JSON.parse(legacyCiphertextUri); + + const ciphertext = { + data: shallowCiphertext.data, + iv: shallowCiphertext.iv + } as any; + + if(shallowCiphertext.salt){ + ciphertext.salt = shallowCiphertext.salt; + } + + if(shallowCiphertext.sender){ + // deserialize stringified key if it is a string (do the same for recipient and signature) + const key: RawKey = typeof shallowCiphertext.sender === "string" + ? LegacySerializerModule.deserializeKey(shallowCiphertext.sender) + : shallowCiphertext.sender as unknown as RawKey; + + ciphertext.sender = await KeyModule.importKey(key) as PublicKey; + } + + if(shallowCiphertext.recipient){ + const key: RawKey = typeof shallowCiphertext.recipient === "string" + ? LegacySerializerModule.deserializeKey(shallowCiphertext.recipient) + : shallowCiphertext.recipient as unknown as RawKey; + + ciphertext.recipient = await KeyModule.importKey(key) as PublicKey; + } + + if(shallowCiphertext.signature){ + const legacySignature: StandardCiphertext = typeof shallowCiphertext.signature === "string" + ? await LegacySerializerModule.deserializeCiphertext(shallowCiphertext.signature) + : shallowCiphertext.signature as unknown as StandardCiphertext; + + ciphertext.signature = legacySignature; + } + + return ciphertext as Ciphertext; + }, + /** + * Converts legacy challenge string back to a challenge object. + * Old challenge string format: `::::::::` + * + * @param legacyChallengeUri - legacy challenge string + * @returns challenge object + */ + deserializeChallenge: async (legacyChallengeUri: string): Promise => { + const properties = legacyChallengeUri.split("::"); + const challenge = {} as any; + + if(properties.length < 4){ + throw new Error("legacy uri is missing required properties (4)") + } + + challenge.nonce = properties[0]; + challenge.timestamp = Number(properties[1]); + challenge.solution = properties[4]; + + const verifier = properties[2]; + const claimant = properties[3]; + + const verifierRawPublicKey: RawKey = LegacySerializerModule.deserializeKey(verifier); + const claimantRawPublicKey: RawKey = LegacySerializerModule.deserializeKey(claimant); + + challenge.verifier = await KeyModule.importKey(verifierRawPublicKey) as PublicKey; + challenge.claimant = await KeyModule.importKey(claimantRawPublicKey) as PublicKey; + + return challenge as Challenge; + } +} + /** * Prefixes for uri */ @@ -82,7 +232,7 @@ const SerializerPrefix = { /** * Operations for serializing SSASy resources for transport */ -export const SerializerModule = { +const SerializerModule = { PREFIX: SerializerPrefix, /** @@ -90,7 +240,7 @@ export const SerializerModule = { * * The representation has the following format: * - * `ssasy://key?type=&domain=&hash=&salt=&iterations=&c_kty=&c_key_ops=&c_alg=&c_ext=&c_kid=&c_use=&c_k=&c_crv=&c_x=&c_y=&c_d=` + * `ssasy://key?type=value&domain=value&hash=value&salt=value&iterations=value&c_kty=value&c_key_ops=value&c_alg=value&c_ext=value&c_kid=value&c_use=value&c_k=value&c_crv=value&c_x=value&c_y=value&c_d=value` * * Note: Try to keep the order of the parameters as shown above so that keys that are saved * in a database can be easily compared. @@ -110,7 +260,7 @@ export const SerializerModule = { let keyUri: string = SerializerPrefix.URI.KEY; // add type to keyUri - keyUri += `type="${_encodeUriParamValue(rawKey.type)}"`; + keyUri += _constructParam("type", rawKey.type, { first: true }); //! the order of the parameters is important for the key comparison (i.e. database queries) const paramKeys: string[] = [ @@ -138,10 +288,11 @@ export const SerializerModule = { // remove protocol prefix const cleanParam: string = isCryptoValue ? paramKey.slice(SerializerPrefix.PARAM.KEY_CRYPTO.length) : paramKey; - // skip param if it does not exist - const inRawKey = (rawKey as any)[cleanParam] !== undefined; - const inCrypto: boolean = isCryptoValue && (rawKey.crypto as any)[cleanParam] !== undefined; - if (!inRawKey && !inCrypto) { + // skip param if it does not exist in raw key or nested crypto object + if ( + !(rawKey as any)[cleanParam] !== undefined && + !(isCryptoValue && (rawKey.crypto as any)[cleanParam] !== undefined) + ) { continue; } @@ -154,7 +305,7 @@ export const SerializerModule = { } // add param to keyUri - keyUri += `&${paramKey}="${_encodeUriParamValue(paramValue)}"`; + keyUri += _constructParam(paramKey, paramValue); } return keyUri @@ -163,14 +314,16 @@ export const SerializerModule = { * Returns a key object from a key uri (see `serializeKey`) * * @param key - key uri + * @param config - configuration object + * @param config.raw - returns a raw key instead of a secure context key * @returns key * */ - deserializeKey: async (keyUri: string): Promise => { + deserializeKey: async (keyUri: string, config?: { raw: boolean }): Promise => { if (!keyUri) { throw new Error(SERIALIZER_ERROR_MESSAGE.MISSING_KEY_STRING); } - if(typeof keyUri !== "string" || !keyUri.startsWith(SerializerPrefix.URI.KEY)){ + if(typeof keyUri !== "string" || !SerializerChecker.isKeyUri(keyUri)){ throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_KEY_STRING) } @@ -189,34 +342,19 @@ export const SerializerModule = { crypto: {} }; - for(let i = 0; i < keyParams.length; i++){ - // split properties (=) - const property = keyParams[i].split("="); - let key: string = property[0]; - let value: string | string[] = property[1]; + for(const param of keyParams){ + const deconstructedParam = _deconstructParam(param); - // remove quotation marks from value (e.g. key="value") - value = value.slice(1, value.length - 1) + let key: string = deconstructedParam.key; + let value: string | string[] = deconstructedParam.value; - // decode property value - value = _decodeUriParam(value) - - // if value starts with `[` and ends with `]`, it was an array if(value.startsWith("[") && value.endsWith("]")){ - - // remove square bracket - value = value.slice(1, value.length-1) - - //convert value into array - value = value.split(","); + value = value.slice(1, -1).split(","); } - - // if key starts with `SerializerPrefix.PARAM.KEY_CRYPTO`, it belongs to nested crypto object + + // add value to nested crypto object if key starts with crypto prefix if(key.startsWith(SerializerPrefix.PARAM.KEY_CRYPTO)){ - - // remove protocol prefix key = key.slice(SerializerPrefix.PARAM.KEY_CRYPTO.length); - rawKey.crypto[key] = value; } else { rawKey[key] = value; @@ -229,14 +367,15 @@ export const SerializerModule = { // convert raw key to a key instance (secure context) const rawKey: RawKey = _rebuildRawKey(keyParams); - // convert raw key to key instance (secure context) and return - return await KeyModule.importKey(rawKey); + return config?.raw + ? rawKey + : await KeyModule.importKey(rawKey); }, /** * Returns a uri string representation of a challenge. * * The representation has the following format: - * `ssasy://challenge?nonce=&solution=×tamp=&verifier=&claimant=` + * `ssasy://challenge?nonce=value&solution=value×tamp=value&verifier=value&claimant=value` * * @param challenge - the challenge to convert to a string * @returns challenge in string format @@ -251,23 +390,23 @@ export const SerializerModule = { let challengeUri = SerializerPrefix.URI.CHALLENGE; // add nonce - challengeUri += `nonce="${_encodeUriParamValue(nonce)}"`; + challengeUri += _constructParam("nonce", nonce, { first: true }); // convert timestamp to string const timestampString = timestamp.toString(); - challengeUri += `×tamp="${_encodeUriParamValue(timestampString)}"`; + challengeUri += _constructParam("timestamp", timestampString); // add verifier const verifierUri = await SerializerModule.serializeKey(verifier); - challengeUri += `&verifier="${_encodeUriParamValue(verifierUri)}"`; + challengeUri += _constructParam("verifier", verifierUri); // add claimant const claimantUri = await SerializerModule.serializeKey(claimant); - challengeUri += `&claimant="${_encodeUriParamValue(claimantUri)}"`; + challengeUri += _constructParam("claimant", claimantUri); // add solution (if exists) if(solution){ - challengeUri += `&solution="${_encodeUriParamValue(solution)}"`; + challengeUri += _constructParam("solution", solution); } return challengeUri; @@ -283,63 +422,81 @@ export const SerializerModule = { throw new Error(SERIALIZER_ERROR_MESSAGE.MISSING_CHALLENGE_STRING); } - if(typeof challengeUri !== "string" || !challengeUri.startsWith(SerializerPrefix.URI.CHALLENGE)){ + if(typeof challengeUri !== "string"){ throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING) } - - const challenge = {} as any; - - // remove challenge protocol prefix - challengeUri = challengeUri.slice(SerializerPrefix.URI.CHALLENGE.length) - // extract all properties - const challengeParams: string[] = challengeUri.split("&") + if(!SerializerChecker.isChallengeUri(challengeUri)){ + /** + * ! Temporary fix for legacy challenge strings + * + * This block needs to handle the edge-case where a challenge uri is + * conforming to the old format: `::::::::` + */ + + let migratedLegacyUri = false; + + + try { + const legacyChallenge: Challenge = await LegacySerializerModule.deserializeChallenge(challengeUri); + + challengeUri = await SerializerModule.serializeChallenge(legacyChallenge); + + migratedLegacyUri = true; + + } catch (error) { + throw new Error(SERIALIZER_ERROR_MESSAGE.LEGACY_INVALID_CHALLENGE_STRING) + } + + if(!migratedLegacyUri){ + throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING) + } + } /** * Returns a typed challenge value based on key string */ async function _getTypedValue(key: string, value: string): Promise { - try { - if(key === "nonce" || key === "solution"){ - return value as string; - } else if(key === "timestamp"){ - return Number(value) as number - } else if(key === "verifier" || key === "claimant"){ - return await SerializerModule.deserializeKey(value) as PublicKey; - } else { - throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING) - } - } catch (error) { + if(key === "nonce" || key === "solution"){ + return value as string; + } else if(key === "timestamp"){ + return Number(value) as number + } else if(key === "verifier" || key === "claimant"){ + return await SerializerModule.deserializeKey(value) as PublicKey; + } else { throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING) } } - - // rebuild challenge object - for(let i = 0; i < challengeParams.length; i++){ - const param = challengeParams[i]; - const key = param.split("=")[0]; - let value = param.split("=")[1]; - // remove quotation marks from value (e.g. key="value") - value = value.slice(1, value.length - 1) + async function rebuildChallenge(challengeParams: string[]): Promise { + const challenge: any = {}; - // decode param value - value = _decodeUriParam(value) + for(const param of challengeParams){ + const { key, value } = _deconstructParam(param); + + // get typed value + const typedValue = await _getTypedValue(key, value); + + challenge[key] = typedValue; + } - // get typed value - const typedValue = await _getTypedValue(key, value); - - challenge[key] = typedValue; + return challenge as Challenge; } + + // remove challenge protocol prefix + challengeUri = challengeUri.slice(SerializerPrefix.URI.CHALLENGE.length) - return challenge as Challenge; + // extract all properties + const challengeParams: string[] = challengeUri.split("&") + + return await rebuildChallenge(challengeParams); }, /** * Returns a uri string representation of a ciphertext. * * The representation has the following format: - * - standard ciphertext: `ssasy://ciphertext?data=&iv=&salt=` - * - advanced ciphertext: `ssasy://ciphertext?data=&iv=&salt=&sender=&recipient=&signature=` + * - standard ciphertext: `ssasy://ciphertext?data=value&iv=value&salt=value` + * - advanced ciphertext: `ssasy://ciphertext?data=value&iv=value&salt=value&sender=value&recipient=value&signature=value` * * @param ciphertext - the ciphertext to convert to a string */ @@ -351,14 +508,14 @@ export const SerializerModule = { let ciphertextUri = `${SerializerPrefix.URI.CIPHERTEXT}`; // add data to ciphertext string - ciphertextUri += `data="${_encodeUriParamValue(ciphertext.data)}"`; + ciphertextUri += _constructParam("data", ciphertext.data, { first: true }); // add iv to ciphertext string - ciphertextUri += `&iv="${_encodeUriParamValue(ciphertext.iv)}"`; + ciphertextUri += _constructParam("iv", ciphertext.iv); // add salt to ciphertext string (if salt exists) if(ciphertext.salt) { - ciphertextUri += `&salt="${_encodeUriParamValue(ciphertext.salt)}"`; + ciphertextUri += _constructParam("salt", ciphertext.salt); } // add sender to ciphertext string (if sender exists) @@ -367,7 +524,7 @@ export const SerializerModule = { const senderUri = await SerializerModule.serializeKey(sender); // add sender to ciphertext string - ciphertextUri += `&sender="${_encodeUriParamValue(senderUri)}"`; + ciphertextUri += _constructParam("sender", senderUri); } // add recipient to ciphertext string (if recipient exists) @@ -376,7 +533,7 @@ export const SerializerModule = { const recipientUri = await SerializerModule.serializeKey(recipient); // add recipient to ciphertext string - ciphertextUri += `&recipient="${_encodeUriParamValue(recipientUri)}"`; + ciphertextUri += _constructParam("recipient", recipientUri); } // add signature to ciphertext string (if signature exists) @@ -385,7 +542,7 @@ export const SerializerModule = { const signatureUri = await SerializerModule.serializeSignature(signature); // add signature to ciphertext string - ciphertextUri += `&signature="${_encodeUriParamValue(signatureUri)}"`; + ciphertextUri += _constructParam("signature", signatureUri); } return ciphertextUri; @@ -400,69 +557,84 @@ export const SerializerModule = { throw new Error(SERIALIZER_ERROR_MESSAGE.MISSING_CIPHERTEXT_STRING); } - if(typeof ciphertextUri !== "string" || !ciphertextUri.startsWith(SerializerPrefix.URI.CIPHERTEXT)){ + if(typeof ciphertextUri !== "string"){ throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING) } - // remove ciphertext protocol prefix - ciphertextUri = ciphertextUri.slice(SerializerPrefix.URI.CIPHERTEXT.length) + if(!SerializerChecker.isCiphertextUri(ciphertextUri)){ + /** + * ! Temporary fix for legacy challenge strings + * + * This block needs to handle the edge-case where a ciphertext uri is + * conforming to the old format: `"{ iv, data, salt?, sender?, recipient?, signature? }"` + */ - // extract all properties - const ciphertextProperties: string[] = ciphertextUri.split("&") + let migratedLegacyUri = false; + + try { + const legacyCiphertext: Ciphertext = await LegacySerializerModule.deserializeCiphertext(ciphertextUri); + + ciphertextUri = await SerializerModule.serializeCiphertext(legacyCiphertext); + migratedLegacyUri = true; + + } catch (error) { + throw new Error(SERIALIZER_ERROR_MESSAGE.LEGACY_INVALID_CIPHERTEXT_STRING) + } + + if(!migratedLegacyUri){ + throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING) + } + } /** * Returns a typed ciphertext value based on key string */ async function _getTypedValue(key: string, value: string): Promise { - try { - if(key === "data" || key === "iv" || key === "salt"){ - return value as string; - - } else if(key === "signature"){ - return await SerializerModule.deserializeSignature(value) as StandardCiphertext; - - } else if(key === "sender" || key === "recipient"){ - return await SerializerModule.deserializeKey(value) as PublicKey; - - } else { - throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING) + if(key === "data" || key === "iv" || key === "salt"){ + return value as string; + + } else if(key === "signature"){ + try { + return await SerializerModule.deserializeSignature(value) as StandardCiphertext; + } catch (error) { + throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_SIGNATURE_STRING) } - } catch (error) { + } else if(key === "sender" || key === "recipient"){ + return await SerializerModule.deserializeKey(value) as PublicKey; + + } else { throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING) } } - // rebuild ciphertext object - const ciphertext: any = {}; + async function _rebuildCiphertext(ciphertextParams: string[]): Promise{ + const ciphertext: any = {}; - for(let i = 0; i < ciphertextProperties.length; i++){ - // get first index of "=" to split property into key and value - // note: this is a workaround for the case that the value contains "=" (i.e. iv="gjhgdfhshgadhfga==") - const equalOperatorIndex = ciphertextProperties[i].indexOf("="); - - // split properties (=) - const key = ciphertextProperties[i].slice(0, equalOperatorIndex); - let value = ciphertextProperties[i].slice(equalOperatorIndex + 1); + for(const param of ciphertextParams){ + const { key, value } = _deconstructParam(param); + + // get typed value + const typedValue = await _getTypedValue(key, value as string); - // remove quotation marks from value (e.g. key="value") - value = value.slice(1, value.length - 1) + ciphertext[key] = typedValue; + } - // decode property value - value = _decodeUriParam(value) + return ciphertext as Ciphertext; + } - // get typed value - const typedValue = await _getTypedValue(key, value); + // remove ciphertext protocol prefix + ciphertextUri = ciphertextUri.slice(SerializerPrefix.URI.CIPHERTEXT.length) - ciphertext[key] = typedValue; - } + // extract all parameters + const ciphertextParams: string[] = ciphertextUri.split("&"); - return ciphertext as Ciphertext; + return await _rebuildCiphertext(ciphertextParams); }, /** * Returns a uri string representation of a signature. * * The representation has the following format: - * `ssasy://signature?data=&iv=` + * `ssasy://signature?data=value&iv=value` * * @param signature - the signature to convert to a string */ @@ -477,20 +649,19 @@ export const SerializerModule = { * @returns signature object * */ deserializeSignature: async (signatureUri: string): Promise => { - if(!signatureUri) { + if(!signatureUri || !SerializerChecker.isSignatureUri(signatureUri)) { throw new Error(SERIALIZER_ERROR_MESSAGE.MISSING_SIGNATURE_STRING); } - if(!signatureUri.startsWith(SerializerPrefix.URI.SIGNATURE)){ - throw new Error(SERIALIZER_ERROR_MESSAGE.INVALID_SIGNATURE_STRING) - } - const ciphertextUri = signatureUri.replace(SerializerPrefix.URI.SIGNATURE, SerializerPrefix.URI.CIPHERTEXT); return await SerializerModule.deserializeCiphertext(ciphertextUri); } }; -function _validCheckerArg(arg: any, prefix: string): boolean { +/** + * Returns true if arg has a valid prefix. + */ +function _hasValidPrefix(arg: any, prefix: string): boolean { if(!arg) { return false; } @@ -506,29 +677,40 @@ function _validCheckerArg(arg: any, prefix: string): boolean { return true; } -function _extractUriParams(uri: string, prefix: string): string[] { +/** + * Returns decoded uri params from a uri string. + */ +function _extractUriParams(uri: string, prefix: string): {key: string, value: string}[] { // remove protocol prefix uri = uri.slice(prefix.length) // extract all properties from key string - const properties = uri.split("&"); + const properties: string[] = uri.split("&"); - return properties; + return properties.map(property => _deconstructParam(property)); } type KeyT = KeyType.Key | KeyType.SecretKey | KeyType.PassKey | KeyType.PublicKey | KeyType.PrivateKey | KeyType.SharedKey; -export const SerializerChecker = { +const SerializerChecker = { + /** + * Returns true if a key uri is valid. + * + * @param keyUri - encoded key uri + * @param config - configuration object + * @param config.type - match key type + * @returns true if key uri is valid + */ isKeyUri: (keyUri: string, config?: { type?: KeyT } ): boolean => { const requiredParams = [ "type", "c_kty", "c_key_ops", "c_ext" ]; const requiredSymmetricParams = [ ...requiredParams, "c_alg", "c_k" ]; const requiredAsymmetricParams = [ ...requiredParams, "c_crv", "c_x", "c_y" ]; // excluding `c_d` (private key) - if(!_validCheckerArg(keyUri, SerializerPrefix.URI.KEY)) { + if(!_hasValidPrefix(keyUri, SerializerPrefix.URI.KEY)) { return false; } - const params = _extractUriParams(keyUri, SerializerPrefix.URI.KEY); + const params: {key: string, value: string}[] = _extractUriParams(keyUri, SerializerPrefix.URI.KEY); // arg must have required params @@ -536,10 +718,7 @@ export const SerializerChecker = { return false; } - let keyType: string = params.find(param => param.startsWith("type="))?.split("=")[1] || ""; - - // remove quotation marks from value (e.g. key="value") - keyType = keyType.slice(1, keyType.length - 1) + const keyType: string | undefined = params.find(param => param.key === "type")?.value; // key type must match `type`, if it is provided if(config?.type && keyType !== config?.type) { @@ -565,15 +744,21 @@ export const SerializerChecker = { return true; }, + /** + * Returns true if a challenge uri is valid. + * + * @param challengeUri - encoded challenge uri + * @returns true if challenge uri is valid + */ isChallengeUri: (challengeUri: string): boolean => { const requiredParams = [ "nonce", "timestamp", "verifier", "claimant" ]; const maxParams = [ ...requiredParams, "solution" ]; - if(!_validCheckerArg(challengeUri, SerializerPrefix.URI.CHALLENGE)) { + if(!_hasValidPrefix(challengeUri, SerializerPrefix.URI.CHALLENGE)) { return false; } - const params: string[] = _extractUriParams(challengeUri, SerializerPrefix.URI.CHALLENGE); + const params: {key: string, value: string}[] = _extractUriParams(challengeUri, SerializerPrefix.URI.CHALLENGE); // arg must have required params if(params.length < requiredParams.length){ @@ -585,6 +770,24 @@ export const SerializerChecker = { return false; } + try { + const nonce: string | undefined = params.find(param => param.key === "nonce")?.value; + const timestamp: string | undefined = params.find(param => param.key === "timestamp")?.value; + const verifier: string | undefined = params.find(param => param.key === "verifier")?.value; + const claimant: string | undefined = params.find(param => param.key === "claimant")?.value; + + if( + (!nonce || !timestamp || !verifier || !claimant) || // required params must exist + !SerializerChecker.isKeyUri(verifier) || // verifier must be a valid key uri + !SerializerChecker.isKeyUri(claimant) // claimant must be a valid key uri + ){ + return false; + } + + } catch (error) { + return false; + } + return true; }, @@ -592,11 +795,11 @@ export const SerializerChecker = { const requiredParams = [ "data", "iv" ]; const maxParamas = [ ...requiredParams, "salt", "sender", "recipient", "signature" ]; - if(!_validCheckerArg(ciphertextUri, SerializerPrefix.URI.CIPHERTEXT)) { + if(!_hasValidPrefix(ciphertextUri, SerializerPrefix.URI.CIPHERTEXT)) { return false; } - const params: string[] = _extractUriParams(ciphertextUri, SerializerPrefix.URI.CIPHERTEXT); + const params: {key: string, value: string}[] = _extractUriParams(ciphertextUri, SerializerPrefix.URI.CIPHERTEXT); // arg must have required params if(params.length < requiredParams.length){ @@ -608,23 +811,73 @@ export const SerializerChecker = { return false; } + try { + const data: string | undefined = params.find(param => param.key === "data")?.value; + const iv: string | undefined = params.find(param => param.key === "iv")?.value; + const sender: string | undefined = params.find(param => param.key === "sender")?.value; + const recipient: string | undefined = params.find(param => param.key === "recipient")?.value; + const signature: string | undefined = params.find(param => param.key === "signature")?.value; + + if( + (data === "" || data === "undefined") || + (iv === "" || iv === "undefined") + ){ + return false; + } + + if(sender && !SerializerChecker.isKeyUri(sender)){ + return false; + } + + if(recipient && !SerializerChecker.isKeyUri(recipient)){ + return false; + } + + if(signature && !SerializerChecker.isSignatureUri(signature)){ + return false; + } + } catch (error) { + return false; + } + + return true; }, isSignatureUri: (signatureUri: string): boolean => { const requiredParams = [ "data", "iv" ]; - if(!_validCheckerArg(signatureUri, SerializerPrefix.URI.SIGNATURE)) { + if(!_hasValidPrefix(signatureUri, SerializerPrefix.URI.SIGNATURE)) { return false; } - const params: string[] = _extractUriParams(signatureUri, SerializerPrefix.URI.SIGNATURE); + const params: {key: string, value: string}[] = _extractUriParams(signatureUri, SerializerPrefix.URI.SIGNATURE); // arg must have required params if(params.length < requiredParams.length){ return false; } + try { + const data: string | undefined = params.find(param => param.key === "data")?.value; + const iv: string | undefined = params.find(param => param.key === "iv")?.value; + + if( + (data === "" || data === "undefined") || + (iv === "" || iv === "undefined") + ){ + return false; + } + } catch (error) { + return false; + } + return true; } +} + +export { + SERIALIZER_ERROR_MESSAGE, + SerializerModule, + SerializerChecker } \ No newline at end of file diff --git a/src/wallet.ts b/src/wallet.ts index 2e83080..cac1f7f 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -16,14 +16,14 @@ import { KeyType } from "./interfaces"; import { SerializerModule, + SerializerChecker, ChallengeModule, ChallengeChecker, CryptoModule, CryptoChecker, - CRYPTO_ERROR_MESSAGE, KeyModule, KeyChecker, - SerializerChecker + CRYPTO_ERROR_MESSAGE } from "./modules"; import type { Ciphertext, @@ -42,15 +42,16 @@ export const WALLET_ERROR_MESSAGE = { INVALID_PAYLOAD: "Payload is invalid (must be a string)", INVALID_CIPHERTEXT: "Ciphertext is invalid", INVALID_CIPHERTEXT_ORIGIN: "Ciphertext sender or recipient does not match the wallet's public key", - INVALID_CIPHERTEXT_SIGNATURE: "Ciphertext signature is invalid or missing", + INVALID_CIPHERTEXT_SIGNATURE: "Ciphertext signature is invalid", INVALID_CHALLENGE_ORIGIN: "Challenge verifier or claimant does not match the wallet's public key", INVALID_SIGNATURE_ORIGIN: "Signature's parties do not match the challenge's parties", + INVALID_SIGNATURE: "Signature was not signed by the wallet's private key", MISSING_KEY: "Key is missing", MISSING_PAYLOAD: "Payload is missing", MISSING_CIPHERTEXT: "Ciphertext is missing", + MISSING_CIPHERTEXT_SIGNATURE: "Ciphertext is missing signature", MISSING_CIPHERTEXT_CHALLENGE: "Ciphertext is missing challenge", - MISSING_CIPHERTEXT_PARTIES: "Ciphertext is missing sender or recipient", - MISSING_SIGNATURE_MESSAGE: "Signature message is missing" + MISSING_CIPHERTEXT_PARTIES: "Ciphertext is missing sender or recipient" }; /** @@ -69,7 +70,7 @@ export type ChallengeResult = { publicKey: string; signature?: string | undefine /** * Returns ciphertext if it contains the correct sender and recipient */ -function _isEncryptedChallenge(ciphertext: unknown): EncryptedChallenge { +function _processEncryptedChallenge(ciphertext: unknown): EncryptedChallenge { if (!ciphertext) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); } @@ -343,61 +344,66 @@ export class Wallet { claimantPublicKey ); - // serialize the challenge - const challengeUri: string = await SerializerModule.serializeChallenge(challenge); - - // get the wallet's public key - const publicKey: PublicKey = await this.getPublicKey({ secure: true }); - // generate a shared key const sharedKey: SharedKey = await KeyModule.generateSharedKey({ privateKey: await this.getPrivateKey(), publicKey: claimantPublicKey }); + // serialize the challenge + const challengeUri: string = await SerializerModule.serializeChallenge(challenge); + + // get the wallet's public key + const publicKey: PublicKey = await this.getPublicKey({ secure: true }); + // encrypt the challenge with the shared key and return it - const ciphertext: AdvancedCiphertext = await CryptoModule.encrypt( + let ciphertext: AdvancedCiphertext = await CryptoModule.encrypt( sharedKey, challengeUri, publicKey, claimantPublicKey ) as AdvancedCiphertext; - // convert ciphertext to string - const ciphertextString: string = await SerializerModule.serializeCiphertext({ - ...ciphertext, - signature: claimantSignatureUri ? await SerializerModule.deserializeSignature(claimantSignatureUri) : undefined - }); + // add the claimant's signature to the ciphertext + if(claimantSignatureUri) { + ciphertext = { + ...ciphertext, + signature: await SerializerModule.deserializeSignature(claimantSignatureUri) + } + } - return ciphertextString; + // return ciphertext as a uri + return await SerializerModule.serializeCiphertext(ciphertext); } /** - * Returns an encrypted challenge-response (a.k.a. solution) + * Returns an encrypted challenge-response for the verifier's challenge. * - * @param ciphertextUri - encrypted challenge uri + * @param encryptedChallengeUri - encrypted challenge uri * @param config - options object * @param config.requireSignature - whether or not the challenge must have a signature * @returns encrypted challenge-response */ - async generateChallengeResponse(ciphertextUri: string, config?: { requireSignature: boolean }): Promise { - if (!ciphertextUri) { - throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + async generateChallengeResponse(encryptedChallengeUri: string, config?: { requireSignature: boolean }): Promise { + if (!encryptedChallengeUri) { + throw new Error(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT); } - const ciphertext: Ciphertext = await SerializerModule.deserializeCiphertext( - ciphertextUri - ); + let deserializedCiphertext: AdvancedCiphertext; + + try { + deserializedCiphertext = await SerializerModule.deserializeCiphertext(encryptedChallengeUri); + } catch (error) { + throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + } + + const encryptedChallenge: EncryptedChallenge = _processEncryptedChallenge(deserializedCiphertext); + - const encryptedChallenge: EncryptedChallenge = _isEncryptedChallenge(ciphertext); - - const publicKey: PublicKey = await this.getPublicKey({ secure: true }); + const walletPublicKey: PublicKey = await this.getPublicKey({ secure: true }); // throw error if the ciphertext is not meant for this wallet - const recipientMatchesWallet: boolean = await KeyChecker.isSameKey( - encryptedChallenge.recipient, - publicKey - ); + const recipientMatchesWallet: boolean = await KeyChecker.isSameKey(encryptedChallenge.recipient, walletPublicKey); if (!recipientMatchesWallet) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_ORIGIN); @@ -409,15 +415,15 @@ export class Wallet { publicKey: encryptedChallenge.sender }); - let challengeString: string; + let decryptedChallengeUri: string; - // decrypt the challenge try { - challengeString = await CryptoModule.decrypt( + decryptedChallengeUri = await CryptoModule.decrypt( sharedKey, encryptedChallenge ); } catch (error) { + // throw error if the shared key fails to decrypt the ciphertext (it means the verifier's public key is wrong) if ((error as Error).message === CRYPTO_ERROR_MESSAGE.WRONG_KEY) { throw new Error(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT_CHALLENGE); } @@ -425,72 +431,60 @@ export class Wallet { throw error; } - // deserialize the challenge - const challenge = await SerializerModule.deserializeChallenge( - challengeString - ); - // inspect signature if it is required if (config?.requireSignature) { + // throw error if the ciphertext does not have a signature if (encryptedChallenge.signature === undefined) { - throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE); + throw new Error(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT_SIGNATURE); } - + // throw error if the signature is not valid if (!CryptoChecker.isCiphertext(encryptedChallenge.signature)) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE); } - - let signedChallenge: Challenge; + + let signatureUri: string; // encrypted challenge uri try { - // encode the ciphertext signature for verification function - const signatureString = await SerializerModule.serializeSignature( - encryptedChallenge.signature - ); - - // decrypt/verify the signature - const challengeString: string | null = await this.verifySignature( - signatureString - ); - - // throw error if signature does not match this wallet's private key - if (challengeString === null) { - throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE); - } - - // deserialize the encrypted - signedChallenge = await SerializerModule.deserializeChallenge( - challengeString - ); + // encode the ciphertext signature for verification function + signatureUri = await SerializerModule.serializeSignature(encryptedChallenge.signature); } catch (error) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE); } + // decrypt/verify the signature (it should be a challenge uri) + const challengeUri: string | null = await this.verifySignature(signatureUri); + + // throw error if signature does not match this wallet's private key + if ((challengeUri === null)) { + throw new Error(WALLET_ERROR_MESSAGE.INVALID_SIGNATURE); + } + + // deserialize the challenge (it might be a legacy challenge) + const signatureChallenge: Challenge = await SerializerModule.deserializeChallenge(challengeUri); + // throw error if the solution is not meant for this wallet - const publicKey: PublicKey = await this.getPublicKey({ secure: true }); - const matchesWallet: boolean = await KeyChecker.isSameKey( - signedChallenge.claimant, - publicKey - ); - const matchesVerifier: boolean = await KeyChecker.isSameKey( - signedChallenge.verifier, - encryptedChallenge.sender - ); - - if (matchesWallet === false || matchesVerifier === false) { + if ( + await KeyChecker.isSameKey(signatureChallenge.claimant, walletPublicKey) == false || // doesn't match wallet + await KeyChecker.isSameKey(signatureChallenge.verifier, encryptedChallenge.sender) == false // doesn't match verifier + ) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_SIGNATURE_ORIGIN); } } + // deserialize the challenge + const challenge: Challenge = await SerializerModule.deserializeChallenge( + decryptedChallengeUri + ); + // throw error if the challenge is invalid if (!ChallengeChecker.isChallenge(challenge)) { throw new Error(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT_CHALLENGE); } // throw error if the challenge is not meant for this wallet - if (!(await KeyChecker.isSameKey(challenge.claimant, publicKey))) { + if (!(await KeyChecker.isSameKey(challenge.claimant, walletPublicKey))) { throw new Error(WALLET_ERROR_MESSAGE.INVALID_CHALLENGE_ORIGIN); } @@ -509,7 +503,7 @@ export class Wallet { const encryptedChallengeResponse: AdvancedCiphertext = await CryptoModule.encrypt( sharedKey, challengeResponseUri, - publicKey, + walletPublicKey, encryptedChallenge.sender ); @@ -535,17 +529,17 @@ export class Wallet { * Returns an object with the claimant's public key and the signature of the solution * if the challenge was solved correctly, otherwise returns null. * - * @param ciphertextUri - ciphertext with a challenge-response payload + * @param encryptedChallengeResponseUri - ciphertext with a challenge-response payload * @returns `{ publicKey, signature? }` */ - async verifyChallengeResponse(ciphertextUri: string): Promise { - if (!ciphertextUri) { - throw new Error(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + async verifyChallengeResponse(encryptedChallengeResponseUri: string): Promise { + if (!encryptedChallengeResponseUri) { + throw new Error(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT); } - const ciphertext = await SerializerModule.deserializeCiphertext(ciphertextUri); + const ciphertext: AdvancedCiphertext = await SerializerModule.deserializeCiphertext(encryptedChallengeResponseUri); - const encrypedChallengeResponse: EncryptedChallenge = _isEncryptedChallenge(ciphertext); + const encrypedChallengeResponse: EncryptedChallenge = _processEncryptedChallenge(ciphertext); const publicKey: PublicKey = await this.getPublicKey({ secure: true }); @@ -574,6 +568,7 @@ export class Wallet { encrypedChallengeResponse ); + challengeResponse = await SerializerModule.deserializeChallenge(decrypedChallengeResponseString); } catch (error) { if ( diff --git a/tests/index.test.ts b/tests/index.test.ts new file mode 100644 index 0000000..3330d02 --- /dev/null +++ b/tests/index.test.ts @@ -0,0 +1,171 @@ +import { expect } from "chai"; +import { Wallet } from "../src/wallet"; +import { SerializerModule, SerializerChecker } from "../src/modules"; +import type { ChallengeResult } from "../src/wallet"; +import type { RawKey, AdvancedCiphertext } from "../src/interfaces"; + +describe("[End-to-End Test Suite]", () => { + + describe("Claimant and verifier engage in a challenge response ritual", () => { + const VERIFIER_PRIVATE_KEY = { + type: "private-key", + crypto: { + key_ops: [ + "deriveKey" + ], + ext: true, + kty: "EC", + x: "TuVgJkd6D3-0S3pLUnxWM9-nYN1wlMCFeNtc3FpFEzw", + y: "vxb6ahvjqG1qvaFqYl8wLJYvFtbj0P1RZxBiuJ2H7VA", + crv: "P-256", + d: "Qa_bQVL9-qF921Ptm0ujs1Hnadn43pC49GPfSIOqMGE" + } + }; + + const CLAIMANT_PRIVATE_KEY = { + type: "private-key", + crypto: { + key_ops: [ + "deriveKey" + ], + ext: true, + kty: "EC", + x: "HQHnwQKj1er9yRKAwBQxnyhl0nGFBaZoClu72rbmVb0", + y: "3oagkoeGui9OuCb6G8DcPwacY0ChK950NT-kPp7qwDY", + crv: "P-256", + d: "iENPCJpw11w1sJykgQzw8B_4vsETAdp6eyjuaga1OW0" + } + }; + + let verifier: Wallet; + let claimant: Wallet; + + beforeEach(async () => { + const verifierKeyUri: string = await SerializerModule.serializeKey(VERIFIER_PRIVATE_KEY as RawKey); + const claimantKeyUri: string = await SerializerModule.serializeKey(CLAIMANT_PRIVATE_KEY as RawKey); + + verifier = new Wallet(verifierKeyUri); + claimant = new Wallet(claimantKeyUri); + }); + + let encryptedChallengeUri: string; + + it("verifier should create an encrypted challenge using claimant's public key uri", async () => { + const claimantPublicKeyUri: string = await claimant.getPublicKey(); + encryptedChallengeUri = await verifier.generateChallenge(claimantPublicKeyUri); + + expect(encryptedChallengeUri).to.be.a("string"); + expect(SerializerChecker.isCiphertextUri(encryptedChallengeUri)).to.be.true; + }); + + let encryptedChallengeResponseUri: string; + + it("claimant should create an encrypted challenge response using verifier's public key uri", async () => { + encryptedChallengeResponseUri = await claimant.generateChallengeResponse(encryptedChallengeUri); + + expect(encryptedChallengeResponseUri).to.be.a("string"); + expect(SerializerChecker.isCiphertextUri(encryptedChallengeResponseUri)).to.be.true; + }); + + it("claimant should add a signature to the challenge response", async () => { + const encryptedChallengeResponse: AdvancedCiphertext = await SerializerModule.deserializeCiphertext(encryptedChallengeResponseUri); + + expect(encryptedChallengeResponse).to.be.an("object"); + expect(encryptedChallengeResponse).to.have.property("signature") + }); + + it("verifier should succesfully verify the challenge response", async () => { + const result: ChallengeResult | null = await verifier.verifyChallengeResponse(encryptedChallengeResponseUri); + + expect(result).to.be.an("object"); + expect(result).to.have.property("publicKey").that.is.string; + expect(result).to.have.property("signature").that.is.string; + + expect(SerializerChecker.isKeyUri(result?.publicKey as string)).to.be.true; + expect(SerializerChecker.isSignatureUri(result?.signature as string)).to.be.true; + }); + }); + + describe("Challenge response ritual using legacy URIs", () => { + + const VERIFIER_PRIVATE_KEY = { + type: "private-key", + crypto: { + key_ops: [ + "deriveKey" + ], + ext: true, + kty: "EC", + x: "TuVgJkd6D3-0S3pLUnxWM9-nYN1wlMCFeNtc3FpFEzw", + y: "vxb6ahvjqG1qvaFqYl8wLJYvFtbj0P1RZxBiuJ2H7VA", + crv: "P-256", + d: "Qa_bQVL9-qF921Ptm0ujs1Hnadn43pC49GPfSIOqMGE" + } + }; + + const CLAIMANT_PRIVATE_KEY = { + "type": "private-key", + "crypto": { + "crv": "P-256", + "d": "Udw4rlpbJX2N5qtiBNtwYnd7Me0ek1BKASEDLQr5UUM", + "ext": true, + "key_ops": [ + "deriveKey" + ], + "kty": "EC", + "x": "d9oZ6UPqNeRu3Goq8LC3BjoC2zYcStWoakMDvYEwVn0", + "y": "AFC-mBqsXFcTFl3vMs4L_tTc03j-_OBfefh_deJlJi4" + } + }; + + const CLAIMANT_SIGNATURE = "ssasy://signature?data=t4yCYCwlpGf7%2BRq7X6iDflG%2Fj%2B1zyPI7wcDWwF9rzUZAD2kKdV1cxKlFdPYuVOyG%2FvDa21R0D94xVnQEt0RT4%2B9ojNQRtrNS7VH8sB0J6zX0mdmIyq6aDyhX85RkcoD4UfEUAgUONNPoQZ3yAr58uMG9qyW2Nw5Vha7Mz7JcFRSa2THQcXTsuDvZZQslwJDV8iWph%2F3Fji%2Fq8m79OoGpffFqIUI86%2FZ8AF1n65BMU8ecgm2glFLIImOANFQsWkTLiviOv5vT1AIw0jL3ZI6RGcvuehoJGyHbo3528VG783uzFAmY3iQX%2FUtPcPTsoPar%2F%2FTxhD0kzgBboUYZcZjUPtdvQvMgO2cCg2U%2FZOokqNWqwLM0ex5Ek9xl2mfCsk3De9ZFLqvy61jgEtqOEQGo581TOGlzWZ6ODp8fPgUL6a3N7lhzP4yL9l%2B0jFtfNAlI6jQ98cBraIP0eJkVbO81HQR%2Bmagnve%2FDNA0NXYNsS3uu69jzDvq9WpOwOZvGOxeTSCOfYRxB22Mc%2FoifX5xIGe9LPUzCmAHGDbVfA8OQiPExwUn%2BQ9xyYsLGiVLdnfJpcdPzZCSE9oik5QNmDY0PMfZGGV1SnKZS1mXMAqx9dlRZknpRePAfQsvhiLxToXoiRzn7UG8y0yE56W0Vql7TJBt%2BUt1v25CNNramdgzQlw%3D%3D&iv=JGalPbjLX1VYiRcYZwaBqw%3D%3D"; + + let verifier: Wallet; + let claimant: Wallet; + + beforeEach(async () => { + const verifierKeyUri: string = await SerializerModule.serializeKey(VERIFIER_PRIVATE_KEY as RawKey); + const claimantKeyUri: string = await SerializerModule.serializeKey(CLAIMANT_PRIVATE_KEY as RawKey); + + verifier = new Wallet(verifierKeyUri); + claimant = new Wallet(claimantKeyUri); + }); + + let encryptedChallengeUri: string; + + it("verifier should create an encrypted challenge using claimant's public key uri and the saved user signature", async () => { + const claimantPublicKeyUri: string = await claimant.getPublicKey(); + encryptedChallengeUri = await verifier.generateChallenge(claimantPublicKeyUri, CLAIMANT_SIGNATURE); + + expect(encryptedChallengeUri).to.be.a("string"); + expect(SerializerChecker.isCiphertextUri(encryptedChallengeUri)).to.be.true; + }); + + let encryptedChallengeResponseUri: string; + + it("claimant should create an encrypted challenge response using verifier's public key uri", async () => { + encryptedChallengeResponseUri = await claimant.generateChallengeResponse(encryptedChallengeUri); + + expect(encryptedChallengeResponseUri).to.be.a("string"); + expect(SerializerChecker.isCiphertextUri(encryptedChallengeResponseUri)).to.be.true; + }); + + it("claimant should add a signature to the challenge response", async () => { + const encryptedChallengeResponse: AdvancedCiphertext = await SerializerModule.deserializeCiphertext(encryptedChallengeResponseUri); + + expect(encryptedChallengeResponse).to.be.an("object"); + expect(encryptedChallengeResponse).to.have.property("signature") + }); + + it("verifier should succesfully verify the challenge response", async () => { + const result: ChallengeResult | null = await verifier.verifyChallengeResponse(encryptedChallengeResponseUri); + + expect(result).to.be.an("object"); + expect(result).to.have.property("publicKey").that.is.string; + expect(result).to.have.property("signature").that.is.string; + + expect(SerializerChecker.isKeyUri(result?.publicKey as string)).to.be.true; + expect(SerializerChecker.isSignatureUri(result?.signature as string)).to.be.true; + }); + }); +}); diff --git a/tests/modules/serializer-mod.test.ts b/tests/modules/serializer-mod.test.ts index b1bfbe8..f6bb27f 100644 --- a/tests/modules/serializer-mod.test.ts +++ b/tests/modules/serializer-mod.test.ts @@ -190,15 +190,8 @@ describe("[SerializerModule Test Suite]", () => { for(const property of keyParams) { const value = property.split("=")[1] - // check if value is in quotes - expect(value[0]).to.equal("\""); - expect(value[value.length - 1]).to.equal("\""); - - // remove quotes - const encodedValue = value.slice(1, value.length - 1); - // check if value is encoded - expect(_isValidEncoding(encodedValue)).to.be.true; + expect(_isValidEncoding(value)).to.be.true; } } }); @@ -253,12 +246,19 @@ describe("[SerializerModule Test Suite]", () => { }); it("should deserialize and return a key for all types", async () => { - for(let i = 0; i < serializedKeyChain.length; i++){ - const key = await SerializerModule.deserializeKey(serializedKeyChain[i]) + for(const serializedKey of serializedKeyChain) { + const key = await SerializerModule.deserializeKey(serializedKey); expect(KeyChecker.isKey(key)).to.be.true; } }); + + it("should return a raw key if config.raw is true", async() => { + for(const serializedKey of serializedKeyChain) { + const key = await SerializerModule.deserializeKey(serializedKey, { raw: true }); + expect(KeyChecker.isRawKey(key)).to.be.true; + } + }) }) }) @@ -344,15 +344,8 @@ describe("[SerializerModule Test Suite]", () => { for(const property of challengeResponseParams) { const value = property.split("=")[1] - // check if value is in quotes - expect(value[0]).to.equal("\""); - expect(value[value.length - 1]).to.equal("\""); - - // remove quotes - const encodedValue = value.slice(1, value.length - 1); - // check if value is encoded - expect(_isValidEncoding(encodedValue)).to.be.true; + expect(_isValidEncoding(value)).to.be.true; } }); @@ -363,55 +356,21 @@ describe("[SerializerModule Test Suite]", () => { * 3. should throw an error if invalid verifier is not a public key object */ it("should throw an error if invalid challenge is passed", async () => { - let challengeCopy: Challenge; - - try { - challengeCopy = { - ...challenge, - // empty nonce not allowed - nonce: BufferUtil.BufferToString(new Uint8Array(0)) - }; - await SerializerModule.serializeChallenge(challengeCopy); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE); - } - - try { - challengeCopy = { - ...challenge, - timestamp: "invalid timestamp" as any - }; - await SerializerModule.serializeChallenge(challengeCopy); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE); - } - - try { - challengeCopy = { - ...challenge, - timestamp: "invalid timestamp" as any - }; - await SerializerModule.serializeChallenge(challengeCopy); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE); - } + const invalidChallenges: any[] = [ + { ...challenge, nonce: BufferUtil.BufferToString(new Uint8Array(0)) }, // empty nonce not allowed + { ...challenge,timestamp: "invalid timestamp" }, // timestamp needs to be a number + { ...challenge, verifier: "invalid verifier" }, // verifier needs to be a public key object + { ...challenge, claimant: "invalid claimant" } // claimant needs to be a public key object + ]; - try { - challengeCopy = { - ...challenge, - claimant: "invalid claimant" as any - }; - await SerializerModule.serializeChallenge(challengeCopy); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE); + for(const invalidChallenge of invalidChallenges) { + try { + await SerializerModule.serializeChallenge(invalidChallenge); + expect.fail(TEST_ERROR.DID_NOT_THROW) + } catch (e) { + const error = e as Error; + expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE); + } } }) }) @@ -477,29 +436,24 @@ describe("[SerializerModule Test Suite]", () => { * 3. should throw an error if invalid verifier is not a public key object */ it("should throw an error if invalid challenge string is passed", async () => { - - try { - await SerializerModule.deserializeChallenge(`invalid-nonce::${challenge.timestamp}::${challenge.verifier}::${challenge.claimant}::${challenge.solution}`); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING); - } + const invalidChallengeStrings = [ + "invalid", + "invalid-nonce::invalid-timestamp::invalid-verifier::invalid-claimant::invalid-solution", + "ssasy://challenge?invalid-nonce::invalid-timestamp::invalid-verifier::invalid-claimant::invalid-solution", + "ssasy://challenge?nonce=undefined×tamp=undefined&verifier=undefined&claimant=undefined&solution=undefined" + ] - try { - await SerializerModule.deserializeChallenge(`${challenge.nonce}::invalid-timestamp::${challenge.verifier}::${challenge.claimant}::${challenge.solution}`); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING); - } + for(const invalidChallengeString of invalidChallengeStrings) { + try { + await SerializerModule.deserializeChallenge(invalidChallengeString); + expect.fail(TEST_ERROR.DID_NOT_THROW) + } catch (error) { - try { - await SerializerModule.deserializeChallenge(`${challenge.nonce}::${challenge.timestamp}::${challenge.verifier}::${challenge.claimant}::${challenge.solution}`); - expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING); + expect((error as Error).message).be.oneOf([ + SERIALIZER_ERROR_MESSAGE.INVALID_CHALLENGE_STRING, + SERIALIZER_ERROR_MESSAGE.LEGACY_INVALID_CHALLENGE_STRING + ]); + } } }) }) @@ -536,16 +490,10 @@ describe("[SerializerModule Test Suite]", () => { }); // set shared key - sharedKey = await KeyModule.generateSharedKey({ - privateKey: verifierKeyPair.private, - publicKey: claimantKeyPair.public - }); + sharedKey = await KeyModule.generateSharedKey({ privateKey: verifierKeyPair.private, publicKey: claimantKeyPair.public }); // set standard ciphertext - standardCiphertext = await CryptoModule.encrypt( - sharedKey, - plaintext - ); + standardCiphertext = await CryptoModule.encrypt(sharedKey, plaintext); // set standard ciphertext with salt standardCiphertextWithSalt = await CryptoModule.encrypt( @@ -643,15 +591,8 @@ describe("[SerializerModule Test Suite]", () => { for(const property of ciphertextParams) { const value = property.split("=")[1] - // check if value is in quotes - expect(value[0]).to.equal("\""); - expect(value[value.length - 1]).to.equal("\""); - - // remove quotes - const encodedValue = value.slice(1, value.length - 1); - // check if value is encoded - expect(_isValidEncoding(encodedValue)).to.be.true; + expect(_isValidEncoding(value)).to.be.true; } }); @@ -663,15 +604,8 @@ describe("[SerializerModule Test Suite]", () => { for(const property of ciphertextParams) { const value = property.split("=")[1] - // check if value is in quotes - expect(value[0]).to.equal("\""); - expect(value[value.length - 1]).to.equal("\""); - - // remove quotes - const encodedValue = value.slice(1, value.length - 1); - // check if value is encoded - expect(_isValidEncoding(encodedValue)).to.be.true; + expect(_isValidEncoding(value)).to.be.true; } } }); @@ -679,20 +613,10 @@ describe("[SerializerModule Test Suite]", () => { it("should throw an error if invalid ciphertext is passed", async () => { const invalidCiphertexts = [ "invalid", - { - ...standardCiphertext, data: 123 - }, - { - ...standardCiphertext, sender: "invalid-public-key" - }, - { - ...advancedCiphertext, - data: 123 // invalid data - }, - { - ...advancedCiphertext, - sender: "invalid-public-key" // invalid sender - } + { ...standardCiphertext, data: 123 }, + { ...standardCiphertext, sender: "invalid-public-key" }, + { ...advancedCiphertext, data: 123 }, // invalid data + { ...advancedCiphertext, sender: "invalid-public-key" } // invalid sender ] for (const invalidCiphertext of invalidCiphertexts) { @@ -713,6 +637,9 @@ describe("[SerializerModule Test Suite]", () => { let standardCiphertextWithSaltString: string; let advancedCiphertextString: string; let advancedCiphertextWithSignatureString: string; + + let legacyCiphertextString: string; + let legacyAdvancedCiphertextString: string; // with signature before(async () => { standardCiphertextString = await SerializerModule.serializeCiphertext(standardCiphertext); @@ -720,6 +647,17 @@ describe("[SerializerModule Test Suite]", () => { advancedCiphertextString = await SerializerModule.serializeCiphertext(advancedCiphertext); advancedCiphertextWithSignatureString = await SerializerModule.serializeCiphertext(advancedCiphertextWithSignature); + // set legacy ciphertext + const standardCiphertextCopy = { ...standardCiphertext }; + legacyCiphertextString = JSON.stringify(standardCiphertextCopy); + + // set legacy advanced ciphertext + const advancedCiphertextCopy: any = { ...advancedCiphertextWithSignature }; + advancedCiphertextCopy.sender = advancedCiphertextCopy.sender ? await KeyModule.exportKey(advancedCiphertextCopy.sender) : undefined; + advancedCiphertextCopy.recipient = advancedCiphertextCopy.recipient ? await KeyModule.exportKey(advancedCiphertextCopy.recipient) : undefined; + advancedCiphertextCopy.signature = advancedCiphertextCopy.signature ? JSON.stringify(advancedCiphertextCopy.signature) : undefined; + legacyAdvancedCiphertextString = JSON.stringify(advancedCiphertextCopy); + ciphertextStrings = [ standardCiphertextString, standardCiphertextWithSaltString, @@ -753,24 +691,40 @@ describe("[SerializerModule Test Suite]", () => { expect(deserializedAdvancedCipherTextWithSignature.signature?.iv).to.deep.equal(advancedCiphertextWithSignature.signature?.iv); }); - it("should throw an error if invalid ciphertext string is passed", async () => { + it("should support legacy ciphertexts", async () => { + const legacyCiphertexts = [ + legacyCiphertextString, + legacyAdvancedCiphertextString + ] + + for (const legacyCiphertext of legacyCiphertexts) { + const ciphertext = await SerializerModule.deserializeCiphertext(legacyCiphertext); + expect(CryptoChecker.isCiphertext(ciphertext)).to.be.true; + } + }) + + it("should throw an error if invalid ciphertext uri is passed", async () => { const invalidCiphertextStrings = [ "invalid", - JSON.stringify({ - ...advancedCiphertext, data: 123 - }), - JSON.stringify({ - ...advancedCiphertext, sender: "invalid-public-key" - }) + JSON.stringify({ ...advancedCiphertext, data: 123 }), + JSON.stringify({ ...advancedCiphertext, sender: "invalid-public-key" }), + "ssasy://ciphertext?data=undefined&iv=undefined&sender=undefined&recipient=undefined&signature=undefined", + "ssasy://key?type=public-key&c_crv=undefined&c_x=undefined&c_y=undefined&c_kty=undefined&c_key_ops=undefined&c_ext=undefined", + advancedCiphertextString.replace(/data=[^&]*/g, "data=undefined"), // invalid data + advancedCiphertextString.replace(/&sender=[^&]*/g, "&sender=undefined"), // invalid sender + advancedCiphertextWithSignatureString.replace(/&signature=[^&]*/g, "&signature=invalid") // invalid signature ] for (const invalidCiphertextString of invalidCiphertextStrings) { try { await SerializerModule.deserializeCiphertext(invalidCiphertextString); expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING); + } catch (error) { + + expect((error as Error).message).be.oneOf([ + SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING, + SERIALIZER_ERROR_MESSAGE.LEGACY_INVALID_CIPHERTEXT_STRING + ]); } } }) @@ -824,35 +778,21 @@ describe("[SerializerModule Test Suite]", () => { for(const property of signatureParameters) { const value = property.split("=")[1] - // check if value is in quotes - expect(value[0]).to.equal("\""); - expect(value[value.length - 1]).to.equal("\""); - - // remove quotes - const encodedValue = value.slice(1, value.length - 1); - // check if value is encoded - expect(_isValidEncoding(encodedValue)).to.be.true; + expect(_isValidEncoding(value)).to.be.true; } }); it("should throw an error if invalid ciphertext is passed", async () => { const invalidCiphertexts = [ "invalid", - { - ...signature, data: 123 - }, - { - ...signature, sender: "invalid-public-key" - }, - { - ...signature, - data: 123 // invalid data - }, - { - ...signature, - sender: "invalid-public-key" // invalid sender - } + { ...signature, data: 123 }, + { ...signature, sender: "invalid-public-key" }, + { ...signature, data: 123 }, // invalid data + { ...signature, sender: "invalid-public-key" }, // invalid sender + "ssasy://signature?data=undefined&iv=undefined", + "ssasy://ciphertext?data=undefined&iv=undefined", + "ssasy://key?type=public-key&c_crv=undefined&c_x=undefined&c_y=undefined&c_kty=undefined&c_key_ops=undefined&c_ext=undefined" ] for (const invalidCiphertext of invalidCiphertexts) { @@ -882,21 +822,20 @@ describe("[SerializerModule Test Suite]", () => { it("should throw an error if invalid ciphertext string is passed", async () => { const invalidCiphertextStrings = [ "invalid", - JSON.stringify({ - ...signature, data: 123 - }), - JSON.stringify({ - ...signature, sender: "invalid-public-key" - }) + JSON.stringify({ ...signature, data: 123 }), + JSON.stringify({ ...signature, sender: "invalid-public-key" }) ] for (const invalidCiphertextString of invalidCiphertextStrings) { try { await SerializerModule.deserializeCiphertext(invalidCiphertextString); expect.fail(TEST_ERROR.DID_NOT_THROW) - } catch (e) { - const error = e as Error; - expect(error.message).to.equal(SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING); + } catch (error) { + + expect((error as Error).message).be.oneOf([ + SERIALIZER_ERROR_MESSAGE.INVALID_CIPHERTEXT_STRING, + SERIALIZER_ERROR_MESSAGE.LEGACY_INVALID_CIPHERTEXT_STRING + ]); } } }) diff --git a/tests/wallet.test.ts b/tests/wallet.test.ts index d923827..523e52e 100644 --- a/tests/wallet.test.ts +++ b/tests/wallet.test.ts @@ -444,7 +444,7 @@ describe("[Wallet Class Test Suite]", () => { expect.fail(TEST_ERROR.DID_NOT_THROW); } catch (e) { const error = e as Error; - expect(error.message).to.equal(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + expect(error.message).to.equal(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT); } }) @@ -566,29 +566,15 @@ describe("[Wallet Class Test Suite]", () => { describe("generateChallengeResponse() with required signature", () => { let wallet: Wallet; - let challenge: Challenge; // generated by friend wallet let encryptedChallenge: AdvancedCiphertext; // generated by friend wallet let encryptedChallengeString: string; beforeEach(async () => { wallet = new Wallet(testPrivateKeyUri); - - // set challenge - challenge = await ChallengeModule.generateChallenge(friendPrivateKey, testPublicKey); - - // encode challenge - const challengeString = await SerializerModule.serializeChallenge(challenge); - - // derive shared key - const sharedKey = await KeyModule.generateSharedKey({ - privateKey: friendPrivateKey, publicKey: testPublicKey - }); - - // encrypt challenge - encryptedChallenge = await CryptoModule.encrypt(sharedKey, challengeString, friendPublicKey, testPublicKey); + const friendWallet: Wallet = new Wallet(friendPrivateKeyUri); - // convert encrypted challenge to string - encryptedChallengeString = await SerializerModule.serializeCiphertext(encryptedChallenge); + encryptedChallengeString = await friendWallet.generateChallenge(await wallet.getPublicKey()); + encryptedChallenge = await SerializerModule.deserializeCiphertext(encryptedChallengeString); }) it("should throw an error if ciphertext does not contain a signature", async () => { @@ -601,37 +587,35 @@ describe("[Wallet Class Test Suite]", () => { expect.fail(TEST_ERROR.DID_NOT_THROW); } catch (err) { const error = err as Error; - expect(error.message).to.equal(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE); + expect(error.message).to.equal(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT_SIGNATURE); } }) it("should throw an error if challenge signature is invalid", async () => { - const encryptedChallengesWithInvalidSignatures = [ - { - ...encryptedChallenge, signature: "invalid" - }, - { - ...encryptedChallenge, signature: undefined as any - }, - { - ...encryptedChallenge, signature: null as any - } + const invalidSignatures = [ + "invalid", + null as any, + undefined as any ]; - for (const encryptedChallenge of encryptedChallengesWithInvalidSignatures) { - try { - const encryptedChallengeString = await SerializerModule.serializeCiphertext(encryptedChallenge); + for (const invalidString of invalidSignatures) { + let encryptedChallengeUri = await SerializerModule.serializeCiphertext(encryptedChallenge); + // inject invalid signature + encryptedChallengeUri += `&signature=${invalidString}` + + try { await wallet.generateChallengeResponse( - encryptedChallengeString, + encryptedChallengeUri, { requireSignature: true } ); - expect.fail(TEST_ERROR.DID_NOT_THROW); - } catch (err) { - const error = err as Error; - const isExpectedMessage = error.message === WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE || error.message === WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT; - expect(isExpectedMessage).to.be.true; + } catch (error) { + expect((error as Error).message).to.be.oneOf([ + WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT_SIGNATURE, + WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT_SIGNATURE, + WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT + ]); } } }) @@ -680,15 +664,15 @@ describe("[Wallet Class Test Suite]", () => { }); - it("should return solution if valid signature is provided", async () => { + it("should return challenge response if valid signature is provided", async () => { const friendWallet: Wallet = new Wallet(friendPrivateKeyUri); - const encryptedFriendChallengeString: string = await friendWallet.generateChallenge(testPublicKeyUri); + const encryptedChallengeUriFromFriend: string = await friendWallet.generateChallenge(testPublicKeyUri); // produce solution with signature - const encryptedChallengeResponseString: string = await wallet.generateChallengeResponse(encryptedFriendChallengeString); + const encryptedChallengeResponseUri: string = await wallet.generateChallengeResponse(encryptedChallengeUriFromFriend); // ... friend saves signature - const response = await friendWallet.verifyChallengeResponse(encryptedChallengeResponseString) + const response = await friendWallet.verifyChallengeResponse(encryptedChallengeResponseUri) // friend generates a new challenge with signature const newEncryptedFriendChallengeString: string = await friendWallet.generateChallenge(testPublicKeyUri, response?.signature); @@ -727,7 +711,7 @@ describe("[Wallet Class Test Suite]", () => { expect.fail(TEST_ERROR.DID_NOT_THROW); } catch (e) { const error = e as Error; - expect(error.message).to.equal(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + expect(error.message).to.equal(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT); } }) @@ -769,7 +753,7 @@ describe("[Wallet Class Test Suite]", () => { expect.fail(TEST_ERROR.DID_NOT_THROW); } catch (e) { const error = e as Error; - expect(error.message).to.equal(WALLET_ERROR_MESSAGE.INVALID_CIPHERTEXT); + expect(error.message).to.equal(WALLET_ERROR_MESSAGE.MISSING_CIPHERTEXT); } })