From 254aa1770eef06fe305dec3e1da672e2992f522d Mon Sep 17 00:00:00 2001 From: Samuel Hellawell Date: Tue, 4 Apr 2023 04:14:21 +0100 Subject: [PATCH 1/2] Fix empty objects in credential throws error Signed-off-by: Samuel Hellawell --- package.json | 2 +- src/anonymous-credentials/schema.ts | 29 ++++++++++++------- src/anonymous-credentials/util.ts | 4 ++- src/bbs-plus/encoder.ts | 10 +++++-- .../anonymous-credentials/credential.spec.ts | 13 +++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index d21bcbfa..774ac106 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/crypto-wasm-ts", - "version": "0.31.2", + "version": "0.31.3", "description": "Typescript abstractions over Dock's Rust crypto library's WASM wrapper", "homepage": "https://github.com/docknetwork/crypto-wasm-ts", "main": "lib/index.js", diff --git a/src/anonymous-credentials/schema.ts b/src/anonymous-credentials/schema.ts index cdbebe5f..4fdd5410 100644 --- a/src/anonymous-credentials/schema.ts +++ b/src/anonymous-credentials/schema.ts @@ -544,6 +544,7 @@ export class CredentialSchema extends Versioned { static validateGeneric(schema: object, ignoreKeys: Set = new Set()) { const [names, values] = this.flattenSchemaObj(schema); + for (let i = 0; i < names.length; i++) { if (ignoreKeys.has(names[i])) { continue; @@ -644,16 +645,16 @@ export class CredentialSchema extends Versioned { properties: { id: { type: 'string' - }, - }, + } + } }, proof: { type: 'object', properties: { type: { type: 'string' - }, - }, + } + } } } }; @@ -1101,13 +1102,19 @@ export class CredentialSchema extends Versioned { } else if (schemaProps[key]['type'] == 'object' && typ == 'object') { const schemaKeys = new Set([...Object.keys(schemaProps[key]['properties'])]); const valKeys = new Set([...Object.keys(value)]); - for (const vk of valKeys) { - CredentialSchema.generateFromCredential(value, schemaProps[key]['properties']); - } - // Delete extra keys not in cred - for (const sk of schemaKeys) { - if (value[sk] === undefined) { - delete schemaKeys[sk]; + + // If empty object, skip it here otherwise causes problems downstream + if (schemaKeys.size === 0) { + delete schemaProps[key]; + } else { + for (const vk of valKeys) { + CredentialSchema.generateFromCredential(value, schemaProps[key]['properties']); + } + // Delete extra keys not in cred + for (const sk of schemaKeys) { + if (value[sk] === undefined) { + delete schemaKeys[sk]; + } } } } else { diff --git a/src/anonymous-credentials/util.ts b/src/anonymous-credentials/util.ts index a4ae2b27..1d3c1e48 100644 --- a/src/anonymous-credentials/util.ts +++ b/src/anonymous-credentials/util.ts @@ -32,6 +32,7 @@ import { ValueType, ValueTypes } from './schema'; import { Encoder } from '../bbs-plus'; import { SetupParam, Statement, WitnessEqualityMetaStatement } from '../composite-proof'; import { SetupParamsTracker } from './setup-params-tracker'; +import { isEmptyObject } from '../util'; export function dockAccumulatorParams(): AccumulatorParams { return Accumulator.generateParams(ACCUMULATOR_PARAMS_LABEL_BYTES); @@ -56,7 +57,8 @@ export function dockSaverEncryptionGensUncompressed(): SaverEncryptionGensUncomp export function flattenTill2ndLastKey(obj: object): [string[], object[]] { const flattened = {}; const temp = flatten(obj) as object; - for (const k of Object.keys(temp)) { + const tempKeys = Object.keys(temp).filter((key) => typeof temp[key] !== 'object' || !isEmptyObject(temp[key])); + for (const k of tempKeys) { // taken from https://stackoverflow.com/a/5555607 const pos = k.lastIndexOf('.'); const name = k.substring(0, pos); diff --git a/src/bbs-plus/encoder.ts b/src/bbs-plus/encoder.ts index 712b8a92..246879c7 100644 --- a/src/bbs-plus/encoder.ts +++ b/src/bbs-plus/encoder.ts @@ -1,5 +1,5 @@ import { SignatureG1 } from './signature'; -import { flattenObjectToKeyValuesList, isPositiveInteger } from '../util'; +import { flattenObjectToKeyValuesList, isEmptyObject, isPositiveInteger } from '../util'; /** * A function that encodes the input to field element bytes @@ -63,7 +63,13 @@ export class Encoder { * @param strict - If set to false and no appropriate encoder is found but the value is a bytearray, it will encode it using the built-in mechanism */ encodeMessageObject(messages: object, strict = false): [string[], Uint8Array[]] { - const [names, values] = flattenObjectToKeyValuesList(messages); + let [names, values] = flattenObjectToKeyValuesList(messages); + + // Filter keys to remove empty objects + // this is done because schema generation removes empty objects + nothing to encode + names = names.filter((k, i) => typeof values[i] !== 'object' || !isEmptyObject(values[i])); + values = values.filter((k, i) => typeof values[i] !== 'object' || !isEmptyObject(values[i])); + const encoded: Uint8Array[] = []; for (let i = 0; i < names.length; i++) { encoded.push(this.encodeMessage(names[i], values[i], strict)); diff --git a/tests/anonymous-credentials/credential.spec.ts b/tests/anonymous-credentials/credential.spec.ts index 5ee25394..4e763f79 100644 --- a/tests/anonymous-credentials/credential.spec.ts +++ b/tests/anonymous-credentials/credential.spec.ts @@ -871,4 +871,17 @@ describe('Credential signing and verification', () => { checkResult(recreatedCred.verify(pk)); } }); + + it('for credential with auto-generated schema and empty objects', () => { + const builder = new CredentialBuilder(); + builder.schema = new CredentialSchema(CredentialSchema.essential()); + builder.subject = { + fname: 'John', + emptyObject: {}, + lname: 'Smith', + }; + + const cred = builder.sign(sk, undefined, {requireSameFieldsAsSchema: false}); + checkResult(cred.verify(pk)); + }); }); From d5fb8255d57273b4f31bb7d9b76916ec972c605f Mon Sep 17 00:00:00 2001 From: Samuel Hellawell Date: Thu, 6 Apr 2023 00:06:17 +0100 Subject: [PATCH 2/2] reformat loop Signed-off-by: Samuel Hellawell --- src/bbs-plus/encoder.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/bbs-plus/encoder.ts b/src/bbs-plus/encoder.ts index 246879c7..fd9d82dc 100644 --- a/src/bbs-plus/encoder.ts +++ b/src/bbs-plus/encoder.ts @@ -63,18 +63,17 @@ export class Encoder { * @param strict - If set to false and no appropriate encoder is found but the value is a bytearray, it will encode it using the built-in mechanism */ encodeMessageObject(messages: object, strict = false): [string[], Uint8Array[]] { - let [names, values] = flattenObjectToKeyValuesList(messages); - - // Filter keys to remove empty objects - // this is done because schema generation removes empty objects + nothing to encode - names = names.filter((k, i) => typeof values[i] !== 'object' || !isEmptyObject(values[i])); - values = values.filter((k, i) => typeof values[i] !== 'object' || !isEmptyObject(values[i])); - + const [names, values] = flattenObjectToKeyValuesList(messages); const encoded: Uint8Array[] = []; - for (let i = 0; i < names.length; i++) { - encoded.push(this.encodeMessage(names[i], values[i], strict)); + for (let i = names.length - 1; i >= 0; i--) { + // Filter to remove empty objects, this is done because schema generation removes empty objects + nothing to encode + if (typeof values[i] !== 'object' || !isEmptyObject(values[i])) { + encoded.push(this.encodeMessage(names[i], values[i], strict)); + } else { + names.splice(i, 1); + } } - return [names, encoded]; + return [names, encoded.reverse()]; } encodeDefault(value: unknown, strict = false): Uint8Array {