UNPKG

@iden3/js-iden3-core

Version:

Low level API to create and manipulate iden3 Claims.

1,363 lines (1,352 loc) 69 kB
import { poseidon, sha256, Hex, base58FromBytes, base58ToBytes } from '@iden3/js-crypto'; const Constants = Object.freeze({ ERRORS: { // ErrDataOverflow means that given *big.Int value does not fit in Field Q // e.g. greater than Q constant: // Q constant: 21888242871839275222246405745257275088548364400416034343698204186575808495617 DATA_OVERFLOW: new Error('data does not fits SNARK size'), // ErrIncorrectIDPosition means that passed position is not one of predefined: // IDPositionIndex or IDPositionValue INCORRECT_ID_POSITION: new Error('incorrect ID position'), // throws when ID not found in the Claim. NO_ID: new Error('ID is not set'), // throws when subject position flags sets in invalid value. INVALID_SUBJECT_POSITION: new Error('invalid subject position'), // ErrIncorrectMerklizePosition means that passed position is not one of predefined: // MerklizePositionIndex or MerklizePositionValue INCORRECT_MERKLIZED_POSITION: new Error('incorrect Merklize position'), // ErrNoMerklizedRoot returns when Merklized Root is not found in the Claim. NO_MERKLIZED_ROOT: new Error('Merklized root is not set'), NETWORK_NOT_SUPPORTED_FOR_DID: new Error('network in not supported for did'), UNSUPPORTED_BLOCKCHAIN_FOR_DID: new Error('not supported blockchain for did'), UNSUPPORTED_DID_METHOD: new Error('not supported DID method'), UNKNOWN_DID_METHOD: new Error('unknown DID method'), INCORRECT_DID: new Error('incorrect DID'), UNSUPPORTED_ID: new Error('unsupported Id') }, SCHEMA: { HASH_LENGTH: 16 }, ETH_ADDRESS_LENGTH: 20, BYTES_LENGTH: 32, ELEM_BYTES_LENGTH: 4, NONCE_BYTES_LENGTH: 8, Q: BigInt('21888242871839275222246405745257275088548364400416034343698204186575808495617'), ID: { TYPE_DEFAULT: Uint8Array.from([0x00, 0x00]), TYPE_READONLY: Uint8Array.from([0b00000000, 0b00000001]), ID_LENGTH: 31 }, DID: { DID_SCHEMA: 'did' }, GENESIS_LENGTH: 27 }); const Blockchain = { Ethereum: 'eth', Polygon: 'polygon', Privado: 'privado', Billions: 'billions', Linea: 'linea', Unknown: 'unknown', NoChain: '', ReadOnly: 'readonly' }; const NetworkId = { Main: 'main', Mumbai: 'mumbai', Amoy: 'amoy', Goerli: 'goerli', Sepolia: 'sepolia', Zkevm: 'zkevm', Cardona: 'cardona', Test: 'test', Unknown: 'unknown', NoNetwork: '' }; const DidMethod = { Iden3: 'iden3', PolygonId: 'polygonid', Other: '' }; /** * Object containing chain IDs for various blockchains and networks. * @type { [key: string]: number } */ const ChainIds = { [`${Blockchain.Ethereum}:${NetworkId.Main}`]: 1, [`${Blockchain.Ethereum}:${NetworkId.Goerli}`]: 5, [`${Blockchain.Ethereum}:${NetworkId.Sepolia}`]: 11155111, [`${Blockchain.Polygon}:${NetworkId.Main}`]: 137, [`${Blockchain.Polygon}:${NetworkId.Mumbai}`]: 80001, [`${Blockchain.Polygon}:${NetworkId.Amoy}`]: 80002, [`${Blockchain.Polygon}:${NetworkId.Zkevm}`]: 1101, [`${Blockchain.Polygon}:${NetworkId.Cardona}`]: 2442, [`${Blockchain.Privado}:${NetworkId.Main}`]: 21000, [`${Blockchain.Privado}:${NetworkId.Test}`]: 21001, [`${Blockchain.Linea}:${NetworkId.Main}`]: 59144, [`${Blockchain.Linea}:${NetworkId.Sepolia}`]: 59141, [`${Blockchain.Billions}:${NetworkId.Main}`]: 45056, [`${Blockchain.Billions}:${NetworkId.Test}`]: 6913 }; const DidMethodByte = { [DidMethod.Iden3]: 0b00000001, [DidMethod.PolygonId]: 0b00000010, [DidMethod.Other]: 0b11111111 }; const blockchainNetworkMap = { [`${Blockchain.ReadOnly}:${NetworkId.NoNetwork}`]: 0b00000000, [`${Blockchain.Polygon}:${NetworkId.Main}`]: 0b0001_0000 | 0b0000_0001, [`${Blockchain.Polygon}:${NetworkId.Mumbai}`]: 0b0001_0000 | 0b0000_0010, [`${Blockchain.Polygon}:${NetworkId.Amoy}`]: 0b0001_0000 | 0b0000_0011, [`${Blockchain.Polygon}:${NetworkId.Zkevm}`]: 0b0001_0000 | 0b0000_0100, [`${Blockchain.Polygon}:${NetworkId.Cardona}`]: 0b0001_0000 | 0b0000_0101, [`${Blockchain.Ethereum}:${NetworkId.Main}`]: 0b0010_0000 | 0b0000_0001, [`${Blockchain.Ethereum}:${NetworkId.Goerli}`]: 0b0010_0000 | 0b0000_0010, [`${Blockchain.Ethereum}:${NetworkId.Sepolia}`]: 0b0010_0000 | 0b0000_0011, [`${Blockchain.Privado}:${NetworkId.Main}`]: 0b1010_0000 | 0b0000_0001, [`${Blockchain.Privado}:${NetworkId.Test}`]: 0b1010_0000 | 0b0000_0010, [`${Blockchain.Linea}:${NetworkId.Main}`]: 0b0100_0000 | 0b0000_1001, [`${Blockchain.Linea}:${NetworkId.Sepolia}`]: 0b0100_0000 | 0b0000_1000, [`${Blockchain.Billions}:${NetworkId.Main}`]: 0b1011_0000 | 0b0000_0001, [`${Blockchain.Billions}:${NetworkId.Test}`]: 0b1011_0000 | 0b0000_0010 }; // DIDMethodNetwork is map for did methods and their blockchain networks const DidMethodNetwork = { [DidMethod.Iden3]: { ...blockchainNetworkMap }, [DidMethod.PolygonId]: { ...blockchainNetworkMap }, [DidMethod.Other]: { [`${Blockchain.Unknown}:${NetworkId.Unknown}`]: 0b1111_1111 } }; // eslint-disable-next-line @typescript-eslint/no-var-requires const encoder = new TextEncoder(); function fromLittleEndian(bytes) { const n256 = BigInt(256); let result = BigInt(0); let base = BigInt(1); bytes.forEach((byte) => { result += base * BigInt(byte); base = base * n256; }); return result; } function fromBigEndian(bytes) { return fromLittleEndian(bytes.reverse()); } function toLittleEndian(bigNumber, len = 31) { const n256 = BigInt(256); const result = new Uint8Array(len); let i = 0; while (bigNumber > BigInt(0)) { result[i] = Number(bigNumber % n256); bigNumber = bigNumber / n256; i += 1; } return result; } function toBigEndian(bigNumber, len = 31) { return toLittleEndian(bigNumber, len).reverse(); } function putUint32(n) { const buf = new ArrayBuffer(4); const view = new DataView(buf); view.setUint32(0, n, true); return new Uint8Array(buf); } function getUint32(arr) { const buf = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength); return new DataView(buf).getUint32(0, true); } function putUint64(n) { const buf = new ArrayBuffer(8); const view = new DataView(buf); view.setBigUint64(0, n, true); return new Uint8Array(buf); } function getUint64(arr) { const buf = arr.buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength); return new DataView(buf).getBigUint64(0, true); } function getUnixTimestamp(d) { return Math.floor(d.getTime() / 1000); } function getDateFromUnixTimestamp(n) { return new Date(n * 1000); } // checkBigIntInField checks if given *big.Int fits in a Field Q element function checkBigIntInField(a) { return a < Constants.Q; } function checkBigIntArrayInField(arr) { return arr.every((n) => checkBigIntInField(n)); } // IdenState calculates the Identity State from the Claims Tree Root, // Revocation Tree Root and Roots Tree Root. function idenState(clr, rer, ror) { return poseidon.hash([clr, rer, ror]); } class StringUtils { static isNotValidIDChar(char) { return (StringUtils.isNotAlpha(char) && StringUtils.isNotDigit(char) && char !== '.' && char !== '-'); } static isNotValidParamChar(char) { return (StringUtils.isNotAlpha(char) && StringUtils.isNotDigit(char) && char !== '.' && char !== '-' && char !== '_' && char !== ':'); } static isNotValidQueryOrFragmentChar(char) { return StringUtils.isNotValidPathChar(char) && char !== '/' && char !== '?'; } static isNotValidPathChar(char) { return StringUtils.isNotUnreservedOrSubdelim(char) && char !== ':' && char !== '@'; } static isNotUnreservedOrSubdelim(char) { switch (char) { case '-': case '.': case '_': case '~': case '!': case '$': case '&': case "'": case '(': case ')': case '*': case '+': case ',': case ';': case '=': return false; default: if (StringUtils.isNotAlpha(char) && StringUtils.isNotDigit(char)) { return true; } return false; } } static isNotHexDigit(char) { return (StringUtils.isNotDigit(char) && (char < '\x41' || char > '\x46') && (char < '\x61' || char > '\x66')); } static isNotDigit(char) { // '\x30' is digit 0, '\x39' is digit 9 return char < '\x30' || char > '\x39'; } // StringUtils.isNotAlpha returns true if a byte is not a big letter between A-Z or small letter between a-z // https://tools.ietf.org/html/rfc5234#appendix-B.1 static isNotAlpha(char) { return StringUtils.isNotSmallLetter(char) && StringUtils.isNotBigLetter(char); } // isNotBigLetter returns true if a byte is not a big letter between A-Z // in US-ASCII http://www.columbia.edu/kermit/ascii.html // https://tools.ietf.org/html/rfc5234#appendix-B.1 static isNotBigLetter(char) { // '\x41' is big letter A, '\x5A' small letter Z return char < '\x41' || char > '\x5A'; } // isNotSmallLetter returns true if a byte is not a small letter between a-z // in US-ASCII http://www.columbia.edu/kermit/ascii.html // https://tools.ietf.org/html/rfc5234#appendix-B.1 static isNotSmallLetter(char) { // '\x61' is small letter a, '\x7A' small letter z return char < '\x61' || char > '\x7A'; } } const genesisFromEthAddress = (addr) => { return Uint8Array.from([...new Uint8Array(7), ...addr]); }; class BytesHelper { static intToBytes(int) { return BytesHelper.intToNBytes(int, Constants.BYTES_LENGTH); } static intToNBytes(int, n) { return Uint8Array.from(toLittleEndian(int, n)); } static checkChecksum(bytes) { const { typ, genesis, checksum } = BytesHelper.decomposeBytes(bytes); if (!checksum.length || JSON.stringify(Uint8Array.from([0, 0])) === JSON.stringify(checksum)) { return false; } const c = BytesHelper.calculateChecksum(typ, genesis); return JSON.stringify(c) === JSON.stringify(checksum); } static decomposeBytes(b) { const offset = 2; const len = b.length - offset; return { typ: b.slice(0, offset), genesis: b.slice(offset, len), checksum: b.slice(-2) }; } static calculateChecksum(typ, genesis) { const toChecksum = [...typ, ...genesis]; const s = toChecksum.reduce((acc, cur) => acc + cur, 0); const checksum = [s >> 8, s & 0xff]; return Uint8Array.from(checksum.reverse()); } static hashBytes(str) { const hash = sha256(encoder.encode(str)); return new Uint8Array(hash); } static hexToBytes(str) { return Hex.decodeString(str); } static bytesToHex(bytes) { const hex = []; for (let i = 0; i < bytes.length; i++) { const current = bytes[i] < 0 ? bytes[i] + 256 : bytes[i]; hex.push((current >>> 4).toString(16)); hex.push((current & 0xf).toString(16)); } return hex.join(''); } static bytesToInt(bytes) { return fromLittleEndian(bytes); } } class ElemBytes { _bytes = new Uint8Array(Constants.BYTES_LENGTH); constructor(bytes) { if (bytes) { this._bytes = bytes; } if (this._bytes.length !== Constants.BYTES_LENGTH) { throw new Error('Invalid bytes length'); } } get bytes() { return this._bytes; } set bytes(value) { this._bytes = value; } toBigInt() { return BytesHelper.bytesToInt(this._bytes); } setBigInt(n) { if (!checkBigIntInField(n)) { throw Constants.ERRORS.DATA_OVERFLOW; } this._bytes = BytesHelper.intToBytes(n); return this; } slotFromHex(hex) { const bytes = Hex.decodeString(hex); if (bytes.length !== Constants.BYTES_LENGTH) { throw new Error('Invalid bytes length'); } this._bytes.set(bytes, 0); return this; } hex() { return Hex.encodeString(this._bytes); } // ElemBytesToInts converts slice of ElemBytes to slice of *big.Int static elemBytesToInts(elements) { const result = []; for (let i = 0; i < elements.length; i++) { const element = elements[i]; result.push(element.toBigInt()); } return result; } static fromInt(i) { if (!checkBigIntInField(i)) { throw Constants.ERRORS.DATA_OVERFLOW; } const bytes = BytesHelper.intToBytes(i); return new ElemBytes(bytes); } } class SchemaHash { // authSchemaHash predefined value of auth schema, used for auth claim during identity creation. // This schema is hardcoded in the identity circuits and used to verify user's auth claim. // Keccak256(https://schema.iden3.io/core/jsonld/auth.jsonld#AuthBJJCredential) last 16 bytes // Hex: cca3371a6cb1b715004407e325bd993c // BigInt: 80551937543569765027552589160822318028 static authSchemaHash = new SchemaHash(Uint8Array.from([204, 163, 55, 26, 108, 177, 183, 21, 0, 68, 7, 227, 37, 189, 153, 60])); _bytes = new Uint8Array(Constants.SCHEMA.HASH_LENGTH); /** * Constructor * @param bytes */ constructor(bytes) { if (bytes) { this._bytes = bytes; } if (this.bytes.length !== Constants.SCHEMA.HASH_LENGTH) { throw new Error(`Schema hash must be ${Constants.SCHEMA.HASH_LENGTH} bytes long`); } } get bytes() { return this._bytes; } /** * MarshalText returns HEX representation of SchemaHash. * @returns {Uint8Array} 32 bytes// */ marshalTextBytes() { return Hex.encode(this.bytes); } marshalText() { return Hex.encodeString(this.bytes); } /** * NewSchemaHashFromHex creates new SchemaHash from hex string * @param s * @returns {SchemaHash} */ static newSchemaHashFromHex(s) { const schemaEncodedBytes = Hex.decodeString(s); if (schemaEncodedBytes.length !== Constants.SCHEMA.HASH_LENGTH) { throw new Error(`invalid schema hash length: ${schemaEncodedBytes.length}`); } return new SchemaHash(schemaEncodedBytes); } /** * NewSchemaHashFromInt creates new SchemaHash from big.Int * @param i * @returns */ static newSchemaHashFromInt(i) { const bytes = BytesHelper.intToNBytes(i, Constants.SCHEMA.HASH_LENGTH); const start = Constants.SCHEMA.HASH_LENGTH - bytes.length; return new SchemaHash(BytesHelper.intToBytes(i).slice(start, Constants.SCHEMA.HASH_LENGTH)); } /** * Convert SchemaHash to big.Int * @returns {bigint} */ bigInt() { return BytesHelper.bytesToInt(this.bytes); } } // ID is a byte array with // [ type | root_genesis | checksum ] // [2 bytes | 27 bytes | 2 bytes ] // where the root_genesis are the first 28 bytes from the hash root_genesis class Id { _bytes; _checksum; constructor(typ, genesis) { this._checksum = BytesHelper.calculateChecksum(typ, genesis); this._bytes = Uint8Array.from([...typ, ...genesis, ...this._checksum]); } static getFromBytes(bytes) { const { typ, genesis } = BytesHelper.decomposeBytes(bytes); return new Id(typ, genesis); } checksum() { return this._checksum; } string() { return base58FromBytes(this._bytes); } get bytes() { return this._bytes; } set bytes(b) { this._bytes = b; } type() { return this._bytes.slice(0, 2); } bigInt() { return fromLittleEndian(this._bytes); } equal(id) { return JSON.stringify(this._bytes) === JSON.stringify(id.bytes); } marshal() { return new TextEncoder().encode(this.string()); } static unMarshal(b) { return Id.fromString(new TextDecoder().decode(b)); } static fromBytes(b) { const bytes = b ?? Uint8Array.from([]); if (bytes.length !== Constants.ID.ID_LENGTH) { throw new Error('fromBytes error: byte array incorrect length'); } if (bytes.every((i) => i === 0)) { throw new Error('fromBytes error: byte array empty'); } const id = Id.getFromBytes(bytes); if (!BytesHelper.checkChecksum(bytes)) { throw new Error('fromBytes error: checksum error'); } return id; } static fromString(s) { const bytes = base58ToBytes(s); return Id.fromBytes(bytes); } static fromBigInt(bigInt) { const b = BytesHelper.intToNBytes(bigInt, Constants.ID.ID_LENGTH); return Id.fromBytes(b); } static profileId(id, nonce) { const bigIntHash = poseidon.hash([id.bigInt(), nonce]); const { typ } = BytesHelper.decomposeBytes(id.bytes); const genesis = BytesHelper.intToNBytes(bigIntHash, 27); return new Id(typ, genesis); } // IdGenesisFromIdenState calculates the genesis ID from an Identity State. static idGenesisFromIdenState(typ, //nolint:revive state) { const idenStateData = ElemBytes.fromInt(state); // we take last 27 bytes, because of swapped endianness const idGenesisBytes = idenStateData.bytes.slice(idenStateData.bytes.length - 27); return new Id(typ, idGenesisBytes); } static ethAddressFromId(id) { const isZeros = id.bytes.slice(2, 2 + 7).every((i) => i === 0); if (!isZeros) { throw new Error("can't get Ethereum address: high bytes of genesis are not zero"); } return id.bytes.slice(2 + 7).slice(0, Constants.ETH_ADDRESS_LENGTH); } } /* Claim structure Index: i_0: [ 128 bits ] claim schema [ 32 bits ] option flags [3] Subject: 000: A.1 Self 001: invalid 010: A.2.i OtherIden Index 011: A.2.v OtherIden Value 100: B.i Object Index 101: B.v Object Value [1] Expiration: bool [1] Updatable: bool [3] Merklized: data is merklized root is stored in the: 000: none 001: C.i Root Index (root located in i_2) 010: C.v Root Value (root located in v_2) [24] 0 [ 32 bits ] version (optional?) [ 61 bits ] 0 - reserved for future use i_1: [ 248 bits] identity (case b) (optional) [ 5 bits ] 0 i_2: [ 253 bits] 0 i_3: [ 253 bits] 0 Value: v_0: [ 64 bits ] revocation nonce [ 64 bits ] expiration date (optional) [ 125 bits] 0 - reserved v_1: [ 248 bits] identity (case c) (optional) [ 5 bits ] 0 v_2: [ 253 bits] 0 v_3: [ 253 bits] 0 */ var SlotName; (function (SlotName) { SlotName["IndexA"] = "IndexA"; SlotName["IndexB"] = "IndexB"; SlotName["ValueA"] = "ValueA"; SlotName["ValueB"] = "ValueB"; })(SlotName || (SlotName = {})); // ErrSlotOverflow means some ElemBytes overflows Q Field. And wraps the name // of overflowed slot. class ErrSlotOverflow extends Error { constructor(msg) { super(`Slot ${msg} not in field (too large)`); Object.setPrototypeOf(this, ErrSlotOverflow.prototype); } } // subjectFlag for the time being describes the location of Id (in index or value // slots or nowhere at all). // // Values subjectFlagInvalid presents for backward compatibility and for now means nothing. var SubjectFlag; (function (SubjectFlag) { SubjectFlag[SubjectFlag["Self"] = 0] = "Self"; SubjectFlag[SubjectFlag["Invalid"] = 1] = "Invalid"; SubjectFlag[SubjectFlag["OtherIdenIndex"] = 2] = "OtherIdenIndex"; SubjectFlag[SubjectFlag["OtherIdenValue"] = 3] = "OtherIdenValue"; })(SubjectFlag || (SubjectFlag = {})); var IdPosition; (function (IdPosition) { IdPosition[IdPosition["None"] = 0] = "None"; IdPosition[IdPosition["Index"] = 1] = "Index"; IdPosition[IdPosition["Value"] = 2] = "Value"; })(IdPosition || (IdPosition = {})); // merklizedFlag for the time being describes the location of root (in index or value // slots or nowhere at all). // // Values merklizedFlagIndex indicates that root is located in index[2] slots. // Values merklizedFlagValue indicates that root is located in value[2] slots. var MerklizedFlag; (function (MerklizedFlag) { MerklizedFlag[MerklizedFlag["None"] = 0] = "None"; MerklizedFlag[MerklizedFlag["Index"] = 32] = "Index"; MerklizedFlag[MerklizedFlag["Value"] = 64] = "Value"; MerklizedFlag[MerklizedFlag["Invalid"] = 128] = "Invalid"; })(MerklizedFlag || (MerklizedFlag = {})); var MerklizedRootPosition; (function (MerklizedRootPosition) { MerklizedRootPosition[MerklizedRootPosition["None"] = 0] = "None"; MerklizedRootPosition[MerklizedRootPosition["Index"] = 1] = "Index"; MerklizedRootPosition[MerklizedRootPosition["Value"] = 2] = "Value"; })(MerklizedRootPosition || (MerklizedRootPosition = {})); var Flags; (function (Flags) { Flags[Flags["ByteIdx"] = 16] = "ByteIdx"; Flags[Flags["ExpirationBitIdx"] = 3] = "ExpirationBitIdx"; Flags[Flags["UpdatableBitIdx"] = 4] = "UpdatableBitIdx"; })(Flags || (Flags = {})); class Claim { _index = []; _value = []; constructor() { for (let i = 0; i < Constants.ELEM_BYTES_LENGTH; i++) { this._index[i] = new ElemBytes(); this._value[i] = new ElemBytes(); } } // NewClaim creates new Claim with specified SchemaHash and any number of // options. Using options you can specify any field in claim. static newClaim(sh, ...args) { const c = new Claim(); c.setSchemaHash(sh); for (let i = 0; i < args.length; i++) { const fn = args[i]; fn(c); } return c; } // GetSchemaHash return copy of claim's schema hash. getSchemaHash() { return new SchemaHash(this._index[0].bytes.slice(0, Constants.SCHEMA.HASH_LENGTH)); } get value() { return this._value; } set value(value) { this._value = value; } get index() { return this._index; } set index(value) { this._index = value; } // SetSchemaHash updates claim's schema hash. setSchemaHash(sh) { this._index[0] = new ElemBytes(Uint8Array.from([...sh.bytes, ...new Array(Constants.SCHEMA.HASH_LENGTH).fill(0)])); } setSubject(s) { // clean first 3 bits this._index[0].bytes[Flags.ByteIdx] &= 0b11111000; this._index[0].bytes[Flags.ByteIdx] |= s; } getSubject() { let sbj = this._index[0].bytes[Flags.ByteIdx]; // clean all except first 3 bits sbj &= 0b00000111; return sbj; } setFlagExpiration(val) { if (val) { this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.ExpirationBitIdx; } else { this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.ExpirationBitIdx); } } getFlagExpiration() { const mask = 0b1 << Flags.ExpirationBitIdx; return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0; } // GetIDPosition returns the position at which the Id is stored. getIdPosition() { switch (this.getSubject()) { case SubjectFlag.Self: return IdPosition.None; case SubjectFlag.OtherIdenIndex: return IdPosition.Index; case SubjectFlag.OtherIdenValue: return IdPosition.Value; default: throw Constants.ERRORS.INVALID_SUBJECT_POSITION; } } // SetValueDataInts sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setValueDataInts(slotA, slotB) { this._value[2] = this.setSlotInt(slotA, SlotName.ValueA); this._value[3] = this.setSlotInt(slotB, SlotName.ValueB); } // SetValueDataBytes sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setValueDataBytes(slotA, slotB) { this._value[2] = this.setSlotBytes(slotA, SlotName.ValueA); this._value[3] = this.setSlotBytes(slotB, SlotName.ValueB); } // SetValueData sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setValueData(slotA, slotB) { const slotsAsInts = [slotA.toBigInt(), slotB.toBigInt()]; if (!checkBigIntArrayInField(slotsAsInts)) { throw Constants.ERRORS.DATA_OVERFLOW; } this._value[2] = slotA; this._value[3] = slotB; } // SetIndexDataInts sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setIndexDataInts(slotA, slotB) { this._index[2] = this.setSlotInt(slotA, SlotName.IndexA); this._index[3] = this.setSlotInt(slotB, SlotName.IndexB); } // SetIndexDataBytes sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setIndexDataBytes(slotA, slotB) { this._index[2] = this.setSlotBytes(slotA, SlotName.IndexA); this._index[3] = this.setSlotBytes(slotB, SlotName.IndexB); } setSlotBytes(value, slotName) { const slot = new ElemBytes(value); if (!checkBigIntInField(slot.toBigInt())) { throw new ErrSlotOverflow(slotName); } return slot; } setFlagMerklized(s) { let f; switch (s) { case MerklizedRootPosition.Index: f = MerklizedFlag.Index; break; case MerklizedRootPosition.Value: f = MerklizedFlag.Value; break; default: f = MerklizedFlag.None; } // clean last 3 bits this.index[0].bytes[Flags.ByteIdx] &= 0b00011111; this.index[0].bytes[Flags.ByteIdx] |= f; } getMerklized() { let mt = this.index[0].bytes[Flags.ByteIdx]; // clean all except last 3 bits mt &= 0b11100000; return mt; } // GetMerklizedPosition returns the position at which the Merklized flag is stored. getMerklizedPosition() { switch (this.getMerklized()) { case MerklizedFlag.None: return MerklizedRootPosition.None; case MerklizedFlag.Index: return MerklizedRootPosition.Index; case MerklizedFlag.Value: return MerklizedRootPosition.Value; default: throw Constants.ERRORS.INCORRECT_MERKLIZED_POSITION; } } setSlotInt(value, slotName) { if (!value) { value = BigInt(0); } if (!checkBigIntInField(value)) { throw new ErrSlotOverflow(slotName); } return new ElemBytes().setBigInt(value); } // SetIndexData sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. setIndexData(slotA, slotB) { const slotsAsInts = [slotA.toBigInt(), slotB.toBigInt()]; if (!checkBigIntArrayInField(slotsAsInts)) { throw Constants.ERRORS.DATA_OVERFLOW; } this._index[2] = slotA; this._index[3] = slotB; } resetExpirationDate() { this.setFlagExpiration(false); const bytes = Array.from({ length: Constants.NONCE_BYTES_LENGTH }, () => 0); const arr = Array.from(this._value[0].bytes); arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes); this._value[0] = new ElemBytes(Uint8Array.from(arr)); } // GetExpirationDate returns expiration date and flag. Flag is true if // expiration date is present, false if null. getExpirationDate() { if (this.getFlagExpiration()) { const unixTimestamp = getUint64(this._value[0].bytes.slice(8, 16)); return getDateFromUnixTimestamp(Number(unixTimestamp)); } return null; } // SetExpirationDate sets expiration date to dt setExpirationDate(dt) { this.setFlagExpiration(true); const bytes = putUint64(BigInt(getUnixTimestamp(dt))); const arr = Array.from(this._value[0].bytes); arr.splice(Constants.NONCE_BYTES_LENGTH, Constants.NONCE_BYTES_LENGTH, ...bytes); this._value[0] = new ElemBytes(Uint8Array.from(arr)); } // GetRevocationNonce returns revocation nonce getRevocationNonce() { return getUint64(this._value[0].bytes.slice(0, 8)); } // SetRevocationNonce sets claim's revocation nonce setRevocationNonce(nonce) { const bytes = putUint64(nonce); if (bytes.length > Constants.NONCE_BYTES_LENGTH) { throw new Error('Nonce length is not valid'); } const arr = Array.from(this._value[0].bytes); arr.splice(0, Constants.NONCE_BYTES_LENGTH, ...bytes); this._value[0] = new ElemBytes(Uint8Array.from(arr)); } getValueId() { return Id.fromBytes(this._value[1].bytes.slice(0, -1)); } // SetValueId sets id to value. Removes id from index if any. setValueId(id) { this.resetIndexId(); this.setSubject(SubjectFlag.OtherIdenValue); const arr = Array.from(this._index[1].bytes); arr.splice(0, id.bytes.length, ...id.bytes); this._value[1] = new ElemBytes(Uint8Array.from(arr)); } resetIndexId() { this._index[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0)); } resetValueId() { this._value[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0)); } getIndexId() { return Id.fromBytes(this._index[1].bytes.slice(0, -1)); } // SetIndexId sets id to index. Removes id from value if any. setIndexId(id) { this.resetValueId(); this.setSubject(SubjectFlag.OtherIdenIndex); const arr = Array.from(this._index[1].bytes); arr.splice(0, id.bytes.length, ...id.bytes); this._index[1] = new ElemBytes(Uint8Array.from(arr)); } // SetVersion sets claim's version setVersion(ver) { const bytes = putUint32(ver); this._index[0].bytes[20] = bytes[0]; this._index[0].bytes[21] = bytes[1]; this._index[0].bytes[22] = bytes[2]; this._index[0].bytes[23] = bytes[3]; } // GetVersion returns claim's version getVersion() { return getUint32(this._index[0].bytes.slice(20, 24)); } // SetFlagUpdatable sets claim's flag `updatable` setFlagUpdatable(val) { if (val) { this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.UpdatableBitIdx; } else { this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.UpdatableBitIdx); } } // HIndex calculates the hash of the Index of the Claim hIndex() { return poseidon.hash(ElemBytes.elemBytesToInts(this._index)); } // GetFlagUpdatable returns claim's flag `updatable` getFlagUpdatable() { const mask = 0b1 << Flags.UpdatableBitIdx; return (this._index[0].bytes[Flags.ByteIdx] & mask) > 0; } // HValue calculates the hash of the Value of the Claim hValue() { return poseidon.hash(ElemBytes.elemBytesToInts(this._value)); } // HiHv returns the HIndex and HValue of the Claim hiHv() { return { hi: this.hIndex(), hv: this.hValue() }; } // SetIndexMerklizedRoot sets merklized root to index. Removes root from value[2] if any. setIndexMerklizedRoot(r) { this.resetValueMerklizedRoot(); this.setFlagMerklized(MerklizedRootPosition.Index); this.index[2] = this.setSlotInt(r, SlotName.IndexA); } resetIndexMerklizedRoot() { this._index[2] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0)); } // SetValueMerklizedRoot sets merklized root to value. Removes root from index[2] if any. setValueMerklizedRoot(r) { this.resetIndexMerklizedRoot(); this.setFlagMerklized(MerklizedRootPosition.Value); this.value[2] = this.setSlotInt(r, SlotName.ValueA); } resetValueMerklizedRoot() { this._value[2] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0)); } // GetMerklizedRoot returns merklized root from claim's index of value. // Returns error ErrNoMerklizedRoot if MerklizedRoot is not set. getMerklizedRoot() { switch (this.getMerklized()) { case MerklizedFlag.Index: return this.index[2].toBigInt(); case MerklizedFlag.Value: return this.value[2].toBigInt(); default: throw Constants.ERRORS.NO_MERKLIZED_ROOT; } } // resetId deletes Id from index and from value. resetId() { this.resetIndexId(); this.resetValueId(); this.setSubject(SubjectFlag.Self); } // GetId returns Id from claim's index of value. // Returns error ErrNoId if Id is not set. getId() { switch (this.getSubject()) { case SubjectFlag.OtherIdenIndex: return this.getIndexId(); case SubjectFlag.OtherIdenValue: return this.getValueId(); default: throw Constants.ERRORS.NO_ID; } } // RawSlots returns raw bytes of claim's index and value rawSlots() { return { index: this._index, value: this._value }; } // RawSlotsAsInts returns slots as []bigint rawSlotsAsInts() { return [...ElemBytes.elemBytesToInts(this._index), ...ElemBytes.elemBytesToInts(this._value)]; } clone() { return JSON.parse(JSON.stringify(this)); } marshalJson() { return this.rawSlotsAsInts().map((b) => b.toString()); } unMarshalJson(b) { const ints = JSON.parse(b).map((s) => BigInt(s)); if (ints.length !== this._index.length + this._value.length) { throw new Error("invalid number of claim's slots"); } this._index = []; this._value = []; for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < ints.length / 2; i++, j++) { this._index[i] = new ElemBytes(); this._index[i].setBigInt(ints[i]); this._value[i] = new ElemBytes(); this._value[i].setBigInt(ints[j]); } return this; } marshalBinary() { const getBytes = (src) => src.reduce((acc, cur) => { return [...acc, ...cur.bytes]; }, []); return Uint8Array.from(getBytes(this._index).concat(getBytes(this._value))); } // Hex returns hex representation of binary claim hex() { const b = this.marshalBinary(); return Hex.encodeString(b); } fromHex(hex) { const b = Hex.decodeString(hex); this.unMarshalBinary(b); return this; } unMarshalBinary(data) { const wantLen = 2 * Constants.ELEM_BYTES_LENGTH * Constants.BYTES_LENGTH; if (data.length !== wantLen) { throw new Error('unexpected length of input data'); } this._index = []; this._value = []; for (let i = 0, j = Constants.ELEM_BYTES_LENGTH; i < Constants.ELEM_BYTES_LENGTH; i++, j++) { this._index[i] = new ElemBytes(data.slice(i * Constants.BYTES_LENGTH, (i + 1) * Constants.BYTES_LENGTH)); this._value[i] = new ElemBytes(data.slice(j * Constants.BYTES_LENGTH, (j + 1) * Constants.BYTES_LENGTH)); } } } class ClaimOptions { // WithFlagUpdatable sets claim's flag `updatable` static withFlagUpdatable(val) { return (c) => c.setFlagUpdatable(val); } // WithVersion sets claim's version static withVersion(ver) { return (c) => c.setVersion(ver); } // WithIndexId sets Id to claim's index static withIndexId(id) { return (c) => c.setIndexId(id); } // WithValueId sets Id to claim's value static withValueId(id) { return (c) => c.setValueId(id); } // WithFlagMerklized sets claim's flag `merklized` static withFlagMerklized(p) { return (c) => c.setFlagMerklized(p); } // WithId sets Id to claim's index or value depending on `pos`. static withId(id, pos) { return (c) => { switch (pos) { case IdPosition.Index: c.setIndexId(id); break; case IdPosition.Value: c.setValueId(id); break; default: throw Constants.ERRORS.INCORRECT_ID_POSITION; } }; } // WithRevocationNonce sets claim's revocation nonce. static withRevocationNonce(nonce) { return (c) => c.setRevocationNonce(nonce); } // WithExpirationDate sets claim's expiration date to `dt`. static withExpirationDate(dt) { return (c) => c.setExpirationDate(dt); } // WithIndexData sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withIndexData(slotA, slotB) { return (c) => c.setIndexData(slotA, slotB); } // WithIndexDataBytes sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withIndexDataBytes(slotA, slotB) { return (c) => c.setIndexDataBytes(slotA, slotB); } // WithIndexDataInts sets data to index slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withIndexDataInts(slotA, slotB) { return (c) => c.setIndexDataInts(slotA, slotB); } // WithValueData sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withValueData(slotA, slotB) { return (c) => c.setValueData(slotA, slotB); } // WithValueDataBytes sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withValueDataBytes(slotA, slotB) { return (c) => c.setValueDataBytes(slotA, slotB); } // WithValueDataInts sets data to value slots A & B. // Returns ErrSlotOverflow if slotA or slotB value are too big. static withValueDataInts(slotA, slotB) { return (c) => c.setValueDataInts(slotA, slotB); } // WithIndexMerklizedRoot sets root to index i_2 // Returns ErrSlotOverflow if root value are too big. static withIndexMerklizedRoot(r) { return (c) => { c.setFlagMerklized(MerklizedRootPosition.Index); c.index[2] = c.setSlotInt(r, SlotName.IndexA); }; } // WithValueMerklizedRoot sets root to value v_2 // Returns ErrSlotOverflow if root value are too big. static withValueMerklizedRoot(r) { return (c) => { c.setFlagMerklized(MerklizedRootPosition.Value); c.value[2] = c.setSlotInt(r, SlotName.ValueA); }; } // WithMerklizedRoot sets root to value v_2 or index i_2 // Returns ErrSlotOverflow if root value are too big. static withMerklizedRoot(r, pos) { return (c) => { switch (pos) { case MerklizedRootPosition.Index: c.setFlagMerklized(MerklizedRootPosition.Index); c.index[2] = c.setSlotInt(r, SlotName.IndexA); break; case MerklizedRootPosition.Value: c.setFlagMerklized(MerklizedRootPosition.Value); c.value[2] = c.setSlotInt(r, SlotName.ValueA); break; default: throw Constants.ERRORS.INCORRECT_MERKLIZED_POSITION; } }; } } // DIDNetworkFlag is a structure to represent DID blockchain and network id class DIDNetworkFlag { blockchain; networkId; constructor(blockchain, networkId) { this.blockchain = blockchain; this.networkId = networkId; } toString() { return `${this.blockchain}:${this.networkId}`; } static fromString(s) { const [blockchain, networkId] = s.split(':'); return new DIDNetworkFlag(blockchain.replace('_', ''), networkId.replace('_', '')); } } // BuildDIDType builds bytes type from chain and network function buildDIDType(method, blockchain, network) { const fb = DidMethodByte[method]; if (!fb) { throw Constants.ERRORS.UNSUPPORTED_DID_METHOD; } const methodFn = DidMethodNetwork[method]; if (!methodFn) { throw Constants.ERRORS.NETWORK_NOT_SUPPORTED_FOR_DID; } const sb = methodFn[new DIDNetworkFlag(blockchain, network).toString()]; if (typeof sb !== 'number') { throw new Error(`blockchain ${blockchain.toString() ?? '-'} and network ${network.toString() ?? '-'} is not defined in core lib`); } return Uint8Array.from([fb, sb]); } // FindNetworkIDForDIDMethodByValue finds network by byte value function findNetworkIDForDIDMethodByValue(method, byteNumber) { const methodMap = DidMethodNetwork[method]; if (!methodMap) { throw Constants.ERRORS.UNSUPPORTED_DID_METHOD; } for (const [key, value] of Object.entries(methodMap)) { if (value === byteNumber) { return DIDNetworkFlag.fromString(key).networkId; } } throw Constants.ERRORS.NETWORK_NOT_SUPPORTED_FOR_DID; } // findBlockchainForDIDMethodByValue finds blockchain type by byte value function findBlockchainForDIDMethodByValue(method, byteNumber) { const methodMap = DidMethodNetwork[method]; if (!methodMap) { throw new Error(`${Constants.ERRORS.NETWORK_NOT_SUPPORTED_FOR_DID}: did method ${method} is not defined in core lib`); } for (const [key, value] of Object.entries(methodMap)) { if (value === byteNumber) { return DIDNetworkFlag.fromString(key).blockchain; } } throw Constants.ERRORS.UNSUPPORTED_BLOCKCHAIN_FOR_DID; } // findDIDMethodByValue finds did method by its byte value function findDIDMethodByValue(byteNumber) { for (const [key, value] of Object.entries(DidMethodByte)) { if (value === byteNumber) { return key; } } throw Constants.ERRORS.UNSUPPORTED_DID_METHOD; } class Param { name; value; constructor(name, value) { this.name = name; this.value = value; } toString() { if (!this.name) { return ''; } if (!this.value) { return this.name; } return `${this.name}=${this.value}`; } } const initDIDParams = Object.freeze({ method: '', id: '', idStrings: [], params: [], path: '', pathSegments: [], query: '', fragment: '' }); class Parser { input; currentIndex = 0; // index in the input which the parser is currently processing: out = { ...initDIDParams }; // the output DID that the parser will assemble as it steps through its state machine // an error in the parser state machine constructor(input) { this.input = input; } checkLength() { const inputLength = this.input.length; if (inputLength < 7) { throw new Error('input length is less than 7'); } return this.parseScheme.bind(this); } // parseScheme is a parserStep that validates that the input begins with 'did:' parseScheme() { const currentIndex = 3; // 4 bytes in 'did:', i.e index 3 // the grammar requires `did:` prefix if (this.input.slice(0, currentIndex + 1) !== 'did:') { throw new Error("input does not begin with 'did:' prefix"); } this.currentIndex = currentIndex; return this.parseMethod.bind(this); } parseMethod() { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; for (;;) { if (currentIndex === inputLength) { // we got to the end of the input and didn't find a second ':' throw new Error('input does not have a second `:` marking end of method name'); } // read the input character at currentIndex const char = input[currentIndex]; if (char === ':') { // we've found the second : in the input that marks the end of the method if (currentIndex === startIndex) { // return error is method is empty, ex- did::1234 throw new Error(`method is empty, ${currentIndex}`); } break; } // as per the grammar method can only be made of digits 0-9 or small letters a-z if (StringUtils.isNotDigit(char) && StringUtils.isNotSmallLetter(char)) { throw new Error(`"character is not a-z OR 0-9, ${currentIndex}`); } // move to the next char currentIndex = currentIndex + 1; } // set parser state this.currentIndex = currentIndex; this.out.method = input.slice(startIndex, currentIndex); // method is followed by specific-idstring, parse that next return this.parseId.bind(this); } parseId() { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; const startIndex = currentIndex; let next = null; for (;;) { if (currentIndex === inputLength) { // we've reached end of input, no next state next = null; break; } const char = input[currentIndex]; if (char === ':') { // encountered : input may have another idstring, parse ID again next = this.parseId; break; } if (char === ';') { // encountered ; input may have a parameter, parse that next next = this.parseParamName; break; } if (char === '/') { // encountered / input may have a path following specific-idstring, parse that next next = this.parsePath; break; } if (char === '?') { // encountered ? input may have a query following specific-idstring, parse that next next = this.parseQuery; break; } if (char === '#') { // encountered # input may have a fragment following specific-idstring, parse that next next = this.parseFragment; break; } // make sure current char is a valid idchar // idchar = ALPHA / DIGIT / "." / "-" if (StringUtils.isNotValidIDChar(char)) { throw new Error(`byte is not ALPHA OR DIGIT OR '.' OR '-', ${currentIndex}`); } // move to the next char currentIndex = currentIndex + 1; } if (currentIndex === startIndex) { // idstring length is zero // from the grammar: // idstring = 1*idchar // return error because idstring is empty, ex- did:a::123:456 throw new Error(`idstring must be at least one char long, ${currentIndex}`); } // set parser state this.currentIndex = currentIndex; this.out.idStrings = [...this.out.idStrings, input.slice(startIndex, currentIndex)]; // return the next parser step return next ? next.bind(this) : null; } parseParamName() { const input = this.input; const startIndex = this.currentIndex + 1; const next = this.paramTransition(); const currentIndex = this.currentIndex; if (currentIndex === startIndex) { throw new Error(`Param name must be at least one char long, ${currentIndex}`); } // Create a new param with the name this.out.params = [...this.out.params, new Param(input.slice(startIndex, currentIndex), '')]; // return the next parser step return next ? next.bind(this) : null; } parseParamValue() { const input = this.input; const startIndex = this.currentIndex + 1; const next = this.paramTransition(); const currentIndex = this.currentIndex; this.out.params[this.out.params.length - 1].value = input.slice(startIndex, currentIndex); return next ? next.bind(this) : null; } paramTransition() { const input = this.input; const inputLength = input.length; let currentIndex = this.currentIndex + 1; let indexIncrement; let next; let percentEncoded; for (;;) { if (currentIndex === inputLength) { // we've reached end of input, no next state next = null; break; } const char = input[currentIndex]; if (char === ';') { // encountered : input may have another param, parse paramName again next = this.parseParamName; break;