@iden3/js-iden3-core
Version:
Low level API to create and manipulate iden3 Claims.
674 lines (599 loc) • 21.5 kB
text/typescript
import { SchemaHash } from './schemaHash';
import { ElemBytes } from './elemBytes';
import { Constants } from './constants';
import { Id } from './id';
import {
checkBigIntArrayInField,
checkBigIntInField,
getDateFromUnixTimestamp,
getUint32,
getUint64,
getUnixTimestamp,
putUint32,
putUint64 as getBytesFromUint64
} from './utils';
import { Hex, poseidon } from '@iden3/js-crypto';
/*
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
*/
export enum SlotName {
IndexA = 'IndexA',
IndexB = 'IndexB',
ValueA = 'ValueA',
ValueB = 'ValueB'
}
// ErrSlotOverflow means some ElemBytes overflows Q Field. And wraps the name
// of overflowed slot.
export class ErrSlotOverflow extends Error {
constructor(msg: string) {
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.
export enum SubjectFlag {
Self = 0b0,
Invalid = 0b1,
OtherIdenIndex = 0b10,
OtherIdenValue = 0b11
}
export enum IdPosition {
None = 0,
Index = 1,
Value = 2
}
// 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.
export enum MerklizedFlag {
None = 0b00000000,
Index = 0b00100000,
Value = 0b01000000,
Invalid = 0b10000000
}
export enum MerklizedRootPosition {
None = 0,
Index = 1,
Value = 2
}
export enum Flags {
ByteIdx = 16,
ExpirationBitIdx = 3,
UpdatableBitIdx = 4
}
export class Claim {
private _index: ElemBytes[] = [];
private _value: ElemBytes[] = [];
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: SchemaHash, ...args: ClaimOption[]): Claim {
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(): SchemaHash {
return new SchemaHash(this._index[0].bytes.slice(0, Constants.SCHEMA.HASH_LENGTH));
}
get value(): ElemBytes[] {
return this._value;
}
set value(value: ElemBytes[]) {
this._value = value;
}
get index(): ElemBytes[] {
return this._index;
}
set index(value: ElemBytes[]) {
this._index = value;
}
// SetSchemaHash updates claim's schema hash.
setSchemaHash(sh: SchemaHash) {
this._index[0] = new ElemBytes(
Uint8Array.from([...sh.bytes, ...new Array(Constants.SCHEMA.HASH_LENGTH).fill(0)])
);
}
setSubject(s: SubjectFlag) {
// clean first 3 bits
this._index[0].bytes[Flags.ByteIdx] &= 0b11111000;
this._index[0].bytes[Flags.ByteIdx] |= s;
}
private getSubject(): SubjectFlag {
let sbj = this._index[0].bytes[Flags.ByteIdx];
// clean all except first 3 bits
sbj &= 0b00000111;
return sbj as SubjectFlag;
}
private setFlagExpiration(val: boolean) {
if (val) {
this._index[0].bytes[Flags.ByteIdx] |= 0b1 << Flags.ExpirationBitIdx;
} else {
this._index[0].bytes[Flags.ByteIdx] &= ~(0b1 << Flags.ExpirationBitIdx);
}
}
private getFlagExpiration(): boolean {
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(): IdPosition {
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: bigint | null, slotB: bigint | null): void {
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: Uint8Array, slotB: Uint8Array): void {
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: ElemBytes, slotB: ElemBytes): void {
const slotsAsInts: bigint[] = [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: bigint | null, slotB: bigint | null): void {
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: Uint8Array | null, slotB: Uint8Array | null): void {
this._index[2] = this.setSlotBytes(slotA, SlotName.IndexA);
this._index[3] = this.setSlotBytes(slotB, SlotName.IndexB);
}
private setSlotBytes(value: Uint8Array | null, slotName: SlotName): ElemBytes {
const slot = new ElemBytes(value);
if (!checkBigIntInField(slot.toBigInt())) {
throw new ErrSlotOverflow(slotName);
}
return slot;
}
setFlagMerklized(s: MerklizedRootPosition): void {
let f: number;
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;
}
private getMerklized(): MerklizedFlag {
let mt = this.index[0].bytes[Flags.ByteIdx];
// clean all except last 3 bits
mt &= 0b11100000;
return mt as MerklizedFlag;
}
// GetMerklizedPosition returns the position at which the Merklized flag is stored.
getMerklizedPosition(): MerklizedRootPosition {
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;
}
}
public setSlotInt(value: bigint | null, slotName: SlotName): ElemBytes {
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: ElemBytes, slotB: ElemBytes) {
const slotsAsInts: bigint[] = [slotA.toBigInt(), slotB.toBigInt()];
if (!checkBigIntArrayInField(slotsAsInts)) {
throw Constants.ERRORS.DATA_OVERFLOW;
}
this._index[2] = slotA;
this._index[3] = slotB;
}
resetExpirationDate(): void {
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(): Date | null {
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: Date) {
this.setFlagExpiration(true);
const bytes = getBytesFromUint64(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(): bigint {
return getUint64(this._value[0].bytes.slice(0, 8));
}
// SetRevocationNonce sets claim's revocation nonce
setRevocationNonce(nonce: bigint): void {
const bytes = getBytesFromUint64(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(): Id {
return Id.fromBytes(this._value[1].bytes.slice(0, -1));
}
// SetValueId sets id to value. Removes id from index if any.
setValueId(id: Id): void {
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));
}
private resetIndexId() {
this._index[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
}
private resetValueId(): void {
this._value[1] = new ElemBytes(new Uint8Array(Constants.BYTES_LENGTH).fill(0));
}
getIndexId(): Id {
return Id.fromBytes(this._index[1].bytes.slice(0, -1));
}
// SetIndexId sets id to index. Removes id from value if any.
setIndexId(id: Id): void {
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: number) {
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(): number {
return getUint32(this._index[0].bytes.slice(20, 24));
}
// SetFlagUpdatable sets claim's flag `updatable`
setFlagUpdatable(val: boolean) {
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(): bigint {
return poseidon.hash(ElemBytes.elemBytesToInts(this._index));
}
// GetFlagUpdatable returns claim's flag `updatable`
getFlagUpdatable(): boolean {
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(): bigint {
return poseidon.hash(ElemBytes.elemBytesToInts(this._value));
}
// HiHv returns the HIndex and HValue of the Claim
hiHv(): { hi: bigint; hv: bigint } {
return { hi: this.hIndex(), hv: this.hValue() };
}
// SetIndexMerklizedRoot sets merklized root to index. Removes root from value[2] if any.
setIndexMerklizedRoot(r: bigint): void {
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: bigint): void {
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(): bigint {
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(): void {
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(): Id {
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(): { index: ElemBytes[]; value: ElemBytes[] } {
return {
index: this._index,
value: this._value
};
}
// RawSlotsAsInts returns slots as []bigint
rawSlotsAsInts(): bigint[] {
return [...ElemBytes.elemBytesToInts(this._index), ...ElemBytes.elemBytesToInts(this._value)];
}
clone(): Claim {
return JSON.parse(JSON.stringify(this));
}
marshalJson(): string[] {
return this.rawSlotsAsInts().map((b) => b.toString());
}
unMarshalJson(b: string): Claim {
const ints: bigint[] = JSON.parse(b).map((s: string) => 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(): Uint8Array {
const getBytes = (src: ElemBytes[]) =>
src.reduce((acc: number[], cur: ElemBytes) => {
return [...acc, ...cur.bytes];
}, []);
return Uint8Array.from(getBytes(this._index).concat(getBytes(this._value)));
}
// Hex returns hex representation of binary claim
hex(): string {
const b = this.marshalBinary();
return Hex.encodeString(b);
}
fromHex(hex: string): Claim {
const b = Hex.decodeString(hex);
this.unMarshalBinary(b);
return this;
}
unMarshalBinary(data: Uint8Array): void {
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)
);
}
}
}
// Option provides the ability to set different Claim's fields on construction
export type ClaimOption = (c: Claim) => void;
export class ClaimOptions {
// WithFlagUpdatable sets claim's flag `updatable`
static withFlagUpdatable(val: boolean): ClaimOption {
return (c: Claim) => c.setFlagUpdatable(val);
}
// WithVersion sets claim's version
static withVersion(ver: number): ClaimOption {
return (c: Claim) => c.setVersion(ver);
}
// WithIndexId sets Id to claim's index
static withIndexId(id: Id): ClaimOption {
return (c: Claim) => c.setIndexId(id);
}
// WithValueId sets Id to claim's value
static withValueId(id: Id): ClaimOption {
return (c: Claim) => c.setValueId(id);
}
// WithFlagMerklized sets claim's flag `merklized`
static withFlagMerklized(p: MerklizedRootPosition): ClaimOption {
return (c: Claim) => c.setFlagMerklized(p);
}
// WithId sets Id to claim's index or value depending on `pos`.
static withId(id: Id, pos: IdPosition): ClaimOption {
return (c: Claim) => {
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: bigint): ClaimOption {
return (c: Claim) => c.setRevocationNonce(nonce);
}
// WithExpirationDate sets claim's expiration date to `dt`.
static withExpirationDate(dt: Date): ClaimOption {
return (c: Claim) => c.setExpirationDate(dt);
}
// WithIndexData sets data to index slots A & B.
// Returns ErrSlotOverflow if slotA or slotB value are too big.
static withIndexData(slotA: ElemBytes, slotB: ElemBytes): ClaimOption {
return (c: Claim) => 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: Uint8Array | null, slotB: Uint8Array | null): ClaimOption {
return (c: Claim) => 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: bigint | null, slotB: bigint | null): ClaimOption {
return (c: Claim) => 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: ElemBytes, slotB: ElemBytes): ClaimOption {
return (c: Claim) => 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: Uint8Array, slotB: Uint8Array): ClaimOption {
return (c: Claim) => 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: bigint | null, slotB: bigint | null): ClaimOption {
return (c: Claim) => c.setValueDataInts(slotA, slotB);
}
// WithIndexMerklizedRoot sets root to index i_2
// Returns ErrSlotOverflow if root value are too big.
static withIndexMerklizedRoot(r: bigint): ClaimOption {
return (c: Claim) => {
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: bigint): ClaimOption {
return (c: Claim) => {
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: bigint, pos: MerklizedRootPosition): ClaimOption {
return (c: Claim) => {
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;
}
};
}
}