@iden3/js-iden3-core
Version:
Low level API to create and manipulate iden3 Claims.
1,363 lines (1,352 loc) • 69 kB
JavaScript
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;