Initial commit with translated description

This commit is contained in:
2026-03-29 13:13:12 +08:00
commit 7dcf9ca28b
23 changed files with 4659 additions and 0 deletions

View File

@@ -0,0 +1,85 @@
const { ethers } = require("ethers");
const { DID } = require("@iden3/js-iden3-core");
const ATTESTATION_SCHEMA_ID =
"0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911"; // bytes32
const ATTESTER_DID = ""; // string
const ATTESTER_IDEN3_ID = 0n; // uint256
const ATTESTER_ETH_ADDRESS = "0x0000000000000000000000000000000000000000"; // address
const EXPIRATION_TIME = 0n; // uint256 — 0 means no expiration
const REVOCABLE = false; // bool
const REF_ID =
"0x0000000000000000000000000000000000000000000000000000000000000000"; // bytes32
const DATA = "0x"; // bytes
function extractIdFromDid(did) {
return DID.idFromDID(DID.parse(did)).bigInt();
}
function buildJsonAttestation(req) {
return {
schemaId: ATTESTATION_SCHEMA_ID,
attester: {
did: ATTESTER_DID,
iden3Id: ATTESTER_IDEN3_ID.toString(),
ethereumAddress: ATTESTER_ETH_ADDRESS,
},
recipient: {
did: req.recipientDid,
iden3Id: extractIdFromDid(req.recipientDid).toString(),
ethereumAddress: req.recipientEthAddress,
},
expirationTime: EXPIRATION_TIME.toString(),
revocable: REVOCABLE,
refId: REF_ID,
data: DATA,
};
}
function buildEncodedAttestation(req) {
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
[
"bytes32", // schemaId
"string", // attester.did
"uint256", // attester.iden3Id
"address", // attester.ethereumAddress
"string", // recipient.did
"uint256", // recipient.iden3Id
"address", // recipient.ethereumAddress
"uint256", // expirationTime
"bool", // revocable
"bytes32", // refId
"bytes", // data
],
[
ATTESTATION_SCHEMA_ID,
ATTESTER_DID,
ATTESTER_IDEN3_ID,
ATTESTER_ETH_ADDRESS,
req.recipientDid,
extractIdFromDid(req.recipientDid),
req.recipientEthAddress,
EXPIRATION_TIME,
REVOCABLE,
REF_ID,
DATA,
],
);
return encoded;
}
function computeAttestationHash(req) {
const hashHex = ethers.keccak256(buildEncodedAttestation(req));
return (
BigInt(hashHex) &
BigInt("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
).toString();
}
module.exports = {
buildEncodedAttestation,
computeAttestationHash,
buildJsonAttestation,
};

149
scripts/shared/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,149 @@
const {
KMS,
Sec256k1Provider,
KmsKeyType,
IdentityWallet,
CredentialStatusType,
EthStateStorage,
CredentialStorage,
IdentityStorage,
InMemoryMerkleTreeStorage,
CredentialStatusResolverRegistry,
RHSResolver,
CredentialWallet,
defaultEthConnectionConfig,
BjjProvider,
} = require("@0xpolygonid/js-sdk");
const { KeysFileStorage } = require("./storage/keys");
const { IdentitiesFileStorage } = require("./storage/identities");
const { DidsFileStorage } = require("./storage/did");
const { ChallengeFileStorage } = require("./storage/challenge");
let cachedRuntime = null;
/**
* Creates and configures the KMS (Key Management System)
*/
async function newInMemoryKMS() {
const memoryKeyStore = new KeysFileStorage("kms.json");
const secpProvider = new Sec256k1Provider(
KmsKeyType.Secp256k1,
memoryKeyStore,
);
const bjjProvider = new BjjProvider(KmsKeyType.BabyJubJub, memoryKeyStore);
const kms = new KMS();
kms.registerKeyProvider(KmsKeyType.Secp256k1, secpProvider);
kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider);
return kms;
}
/**
* Creates Ethereum state storage for Billions Network
*/
function newEthStateStorage(billionsMainnetConfig) {
return new EthStateStorage(billionsMainnetConfig);
}
/**
* Creates data storage with credential, identity, merkle tree, and state storages
*/
function newDataStorage(ethStateStorage) {
return {
credential: new CredentialStorage(
new IdentitiesFileStorage("credentials.json"),
),
identity: new IdentityStorage(
new IdentitiesFileStorage("identities.json"),
new IdentitiesFileStorage("profiles.json"),
),
mt: new InMemoryMerkleTreeStorage(40),
states: ethStateStorage,
};
}
/**
* Creates credential wallet with credential status resolvers
*/
function newCredentialWallet(dataStorage) {
const resolvers = new CredentialStatusResolverRegistry();
resolvers.register(
CredentialStatusType.Iden3ReverseSparseMerkleTreeProof,
new RHSResolver(dataStorage.states),
);
return new CredentialWallet(dataStorage, resolvers);
}
/**
* Creates identity wallet
*/
function newIdentityWallet(kms, dataStorage, credentialWallet) {
return new IdentityWallet(kms, dataStorage, credentialWallet);
}
/**
* Gets Billions Network mainnet configuration
*/
function getBillionsMainnetConfig() {
return {
...defaultEthConnectionConfig,
url: "https://rpc-mainnet.billions.network",
contractAddress: "0x3c9acb2205aa72a05f6d77d708b5cf85fca3a896",
chainId: 45056,
};
}
/**
* Gets default revocation options
*/
function getRevocationOpts() {
return {
type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof,
id: "https://rhs-staging.polygonid.me",
};
}
/**
* Initializes and returns all runtime dependencies.
* Uses caching to avoid re-initialization.
*
* @returns {Promise<Object>} Runtime object containing:
* - kms: Key Management System
* - identityWallet: Identity wallet instance
* - didsStorage: DID storage
* - challengeStorage: Challenge storage
* - billionsMainnetConfig: Billions Network configuration
* - revocationOpts: Revocation options
*/
async function getInitializedRuntime() {
if (cachedRuntime) {
return cachedRuntime;
}
const billionsMainnetConfig = getBillionsMainnetConfig();
const revocationOpts = getRevocationOpts();
const kms = await newInMemoryKMS();
const stateStorage = newEthStateStorage(billionsMainnetConfig);
const dataStorage = newDataStorage(stateStorage);
const credentialWallet = newCredentialWallet(dataStorage);
const identityWallet = newIdentityWallet(kms, dataStorage, credentialWallet);
const didsStorage = new DidsFileStorage("defaultDid.json");
const challengeStorage = new ChallengeFileStorage("challenges.json");
cachedRuntime = {
kms,
identityWallet,
didsStorage,
challengeStorage,
billionsMainnetConfig,
revocationOpts,
};
return cachedRuntime;
}
module.exports = {
getInitializedRuntime,
};

View File

@@ -0,0 +1,35 @@
const fs = require("fs/promises");
const path = require("path");
class FileStorage {
constructor(filename, baseDir = `${process.env.HOME}/.openclaw/billions`) {
this.filePath = path.join(baseDir, filename);
}
async ensureDirectory() {
const dir = path.dirname(this.filePath);
await fs.mkdir(dir, { recursive: true });
}
async readFile() {
try {
const data = await fs.readFile(this.filePath, "utf-8");
return JSON.parse(data);
} catch (error) {
if (error.code === "ENOENT") {
return [];
}
throw error;
}
}
async writeFile(data) {
await this.ensureDirectory();
const json = JSON.stringify(data, null, 2);
const tempPath = `${this.filePath}.tmp`;
await fs.writeFile(tempPath, json, "utf-8");
await fs.rename(tempPath, this.filePath);
}
}
module.exports = { FileStorage };

View File

@@ -0,0 +1,53 @@
const { FileStorage } = require("./base");
class ChallengeFileStorage extends FileStorage {
constructor(filename = "challenges.json") {
super(filename);
}
async save(did, challenge) {
const entries = await this.readFile();
const created_at = new Date();
const index = entries.findIndex((entry) => entry.did === did);
if (index >= 0) {
// Update existing entry
entries[index] = { did, challenge, created_at };
} else {
// Add new entry
entries.push({ did, challenge, created_at });
}
await this.writeFile(entries);
}
async find(did) {
const entries = await this.readFile();
return entries.find((entry) => entry.did === did);
}
async getChallenge(did) {
const entry = await this.find(did);
return entry?.challenge;
}
async list() {
return this.readFile();
}
async delete(did) {
const entries = await this.readFile();
const initialLength = entries.length;
const filtered = entries.filter((entry) => entry.did !== did);
if (filtered.length < initialLength) {
await this.writeFile(filtered);
return true;
}
return false;
}
}
module.exports = { ChallengeFileStorage };

View File

@@ -0,0 +1,81 @@
"use strict";
const crypto = require("crypto");
const ALGORITHM = "aes-256-gcm";
const IV_BYTES = 12;
const TAG_BYTES = 16;
function getMasterKey() {
const rawKey = process.env.BILLIONS_NETWORK_MASTER_KMS_KEY;
if (typeof rawKey !== "string") {
return null;
}
const trimmedKey = rawKey.trim();
// Reject whitespace-only or too-short keys to avoid weak/blank-looking master keys.
// Returning null keeps behavior consistent with the "no key configured" case.
const MIN_MASTER_KEY_LENGTH = 16;
if (trimmedKey.length < MIN_MASTER_KEY_LENGTH) {
return null;
}
return trimmedKey;
}
function deriveAesKey(masterKeyString) {
return crypto.createHash("sha256").update(masterKeyString, "utf8").digest();
}
function encryptKey(keyHex, masterKeyString) {
const aesKey = deriveAesKey(masterKeyString);
const iv = crypto.randomBytes(IV_BYTES);
const cipher = crypto.createCipheriv(ALGORITHM, aesKey, iv, {
authTagLength: TAG_BYTES,
});
const encrypted = Buffer.concat([
cipher.update(keyHex, "utf8"),
cipher.final(),
]);
const authTag = cipher.getAuthTag();
return [
iv.toString("hex"),
authTag.toString("hex"),
encrypted.toString("hex"),
].join(":");
}
function decryptKey(encryptedPayload, masterKeyString) {
const parts = encryptedPayload.split(":");
if (parts.length !== 3) {
throw new Error("Invalid encrypted key format in kms.json");
}
const [ivHex, authTagHex, ciphertextHex] = parts;
const aesKey = deriveAesKey(masterKeyString);
const iv = Buffer.from(ivHex, "hex");
const authTag = Buffer.from(authTagHex, "hex");
const ciphertext = Buffer.from(ciphertextHex, "hex");
const decipher = crypto.createDecipheriv(ALGORITHM, aesKey, iv, {
authTagLength: TAG_BYTES,
});
decipher.setAuthTag(authTag);
try {
return Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]).toString("utf8");
} catch {
throw new Error(
"kms.json decryption failed: wrong BILLIONS_NETWORK_MASTER_KMS_KEY or file has been tampered with",
);
}
}
module.exports = { getMasterKey, encryptKey, decryptKey };

View File

@@ -0,0 +1,47 @@
const { FileStorage } = require("./base");
/**
* DidsFileStorage manages DID entries with default DID support
*/
class DidsFileStorage extends FileStorage {
constructor(filename = "defaultDid.json") {
super(filename);
}
async save({ did, publicKeyHex, isDefault = false }) {
const entries = await this.readFile();
// If setting this as default, unset all other defaults
if (isDefault) {
entries.forEach((entry) => {
entry.isDefault = false;
});
}
const index = entries.findIndex((entry) => entry.did === did);
if (index >= 0) {
entries[index] = { did, publicKeyHex, isDefault };
} else {
entries.push({ did, publicKeyHex, isDefault });
}
await this.writeFile(entries);
}
async find(did) {
const entries = await this.readFile();
return entries.find((entry) => entry.did === did);
}
async getDefault() {
const entries = await this.readFile();
return entries.find((entry) => entry.isDefault);
}
async list() {
return this.readFile();
}
}
module.exports = { DidsFileStorage };

View File

@@ -0,0 +1,44 @@
const { FileStorage } = require("./base");
/**
* IdentitiesFileStorage implements IDataSource<Type> interface from js-sdk
*/
class IdentitiesFileStorage extends FileStorage {
async load() {
return await this.readFile();
}
async save(key, value, keyName = "id") {
const data = await this.readFile();
const index = data.findIndex((item) => item[keyName] === key);
if (index >= 0) {
// Update existing item
data[index] = value;
} else {
// Add new item
data.push(value);
}
await this.writeFile(data);
}
async get(key, keyName = "id") {
const data = await this.readFile();
return data.find((item) => item[keyName] === key);
}
async delete(key, keyName = "id") {
const data = await this.readFile();
const filtered = data.filter((item) => item[keyName] !== key);
if (filtered.length === data.length) {
// Item not found, throw error to match expected behavior
throw new Error(`Item with ${keyName}=${key} not found`);
}
await this.writeFile(filtered);
}
}
module.exports = { IdentitiesFileStorage };

View File

@@ -0,0 +1,117 @@
const { FileStorage } = require("./base");
const { getMasterKey, encryptKey, decryptKey } = require("./crypto");
/**
* File-based storage for cryptographic keys.
* Implements AbstractPrivateKeyStore interface from js-sdk.
* Stores keys in JSON format as an array of per-entry versioned objects.
*/
class KeysFileStorage extends FileStorage {
constructor(filename = "kms.json") {
super(filename);
// Holds raw on-disk entries that could not be decoded in this session
// (e.g. encrypted entries when the master key env var is absent).
// They are round-tripped untouched through writeFile so no data is lost.
this._opaqueEntries = [];
}
_decodeEntry(entry) {
// Legacy format
if (Object.prototype.hasOwnProperty.call(entry, "privateKeyHex")) {
return { alias: entry.alias, privateKeyHex: entry.privateKeyHex };
}
if (entry.version === 1) {
const { alias, key } = entry.data;
const { createdAt } = entry.data;
if (entry.provider === "plain") {
return { alias, privateKeyHex: key, createdAt };
}
if (entry.provider === "encrypted") {
const masterKey = getMasterKey();
if (!masterKey) {
return { alias, _opaque: true, _raw: entry };
}
return { alias, privateKeyHex: decryptKey(key, masterKey), createdAt };
}
}
throw new Error(
`Unrecognised kms.json entry format: ${entry.alias || entry.data.alias || "unknown alias"}}`,
);
}
_encodeEntry({ alias, privateKeyHex, createdAt }) {
const masterKey = getMasterKey();
if (masterKey) {
return {
version: 1,
provider: "encrypted",
data: { alias, key: encryptKey(privateKeyHex, masterKey), createdAt },
};
}
return {
version: 1,
provider: "plain",
data: { alias, key: privateKeyHex, createdAt },
};
}
async readFile() {
const raw = await super.readFile();
if (!Array.isArray(raw)) {
throw new Error("kms.json root must be an array");
}
const decoded = raw.map((entry) => this._decodeEntry(entry));
// Stash raw on-disk objects for entries we cannot decode right now so
// writeFile can round-trip them untouched.
this._opaqueEntries = decoded.filter((e) => e._opaque).map((e) => e._raw);
return decoded.filter((e) => !e._opaque);
}
async writeFile(keys) {
const encoded = keys.map((entry) => this._encodeEntry(entry));
await super.writeFile([...encoded, ...this._opaqueEntries]);
}
async importKey(args) {
const keys = await this.readFile();
const index = keys.findIndex((entry) => entry.alias === args.alias);
if (index >= 0) {
keys[index].privateKeyHex = args.key;
} else {
keys.push({
alias: args.alias,
privateKeyHex: args.key,
createdAt: new Date().toISOString(),
});
}
// update key under alias
this._opaqueEntries = this._opaqueEntries.filter(
(raw) => raw.data?.alias !== args.alias,
);
await this.writeFile(keys);
}
async get(args) {
const keys = await this.readFile();
const entry = keys.find((entry) => entry.alias === args.alias);
return entry ? entry.privateKeyHex : "";
}
async list() {
const keys = await this.readFile();
return keys.map((entry) => ({
alias: entry.alias,
key: entry.privateKeyHex,
}));
}
}
module.exports = { KeysFileStorage };

131
scripts/shared/utils.js Normal file
View File

@@ -0,0 +1,131 @@
const { bytesToHex, keyPath } = require("@0xpolygonid/js-sdk");
const { DID, Id } = require("@iden3/js-iden3-core");
const { v7: uuid } = require("uuid");
const { secp256k1 } = require("@noble/curves/secp256k1");
/**
* Removes the "0x" prefix from a hexadecimal string if it exists
*/
function normalizeKey(keyId) {
return keyId.startsWith("0x") ? keyId.slice(2) : keyId;
}
/**
* Add hex prefix if missing
*/
function addHexPrefix(keyId) {
return keyId.startsWith("0x") ? keyId : `0x${keyId}`;
}
function buildEthereumAddressFromDid(did) {
const ethereumAddress = Id.ethAddressFromId(DID.idFromDID(DID.parse(did)));
return `0x${bytesToHex(ethereumAddress)}`;
}
/**
* Creates a W3C DID document for an Ethereum-based identity
*/
function createDidDocument(did, publicKeyHex) {
return {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
],
id: did,
verificationMethod: [
{
id: `${did}#ethereum-based-id`,
controller: did,
type: "EcdsaSecp256k1RecoveryMethod2020",
ethereumAddress: buildEthereumAddressFromDid(did),
publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex(
true,
),
},
],
authentication: [`${did}#ethereum-based-id`],
};
}
/**
* Generates a normalized key path for storage
*/
function normalizedKeyPath(keyType, keyID) {
return keyPath(keyType, normalizeKey(keyID));
}
/**
* Creates an Authorization Response Message for challenge signing
*/
function getAuthResponseMessage(did, challenge) {
const { PROTOCOL_CONSTANTS } = require("@0xpolygonid/js-sdk");
return {
id: uuid(),
thid: uuid(),
from: did,
to: "",
type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE
.AUTHORIZATION_RESPONSE_MESSAGE_TYPE,
body: {
message: challenge,
scope: [],
},
};
}
/**
* Parses command line arguments into an object
* Example: --did abc --key 123 => { did: 'abc', key: '123' }
*/
function parseArgs() {
const args = {};
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i].startsWith("--")) {
const key = process.argv[i].slice(2);
const value = process.argv[i + 1];
args[key] = value;
i++;
}
}
return args;
}
/**
* Formats an error message for CLI output
*/
function formatError(error) {
return `Error: ${error.message}`;
}
/**
* Outputs success message to stdout
*/
function outputSuccess(data) {
if (typeof data === "string") {
console.log(data);
} else {
console.log(JSON.stringify(data, null, 2));
}
}
function urlFormating(title, url) {
return `[${title}](${url})`;
}
function codeFormating(data) {
return `\\\`\\\`\\\`${data}\\\`\\\`\\\``;
}
module.exports = {
normalizeKey,
addHexPrefix,
createDidDocument,
normalizedKeyPath,
getAuthResponseMessage,
parseArgs,
formatError,
outputSuccess,
buildEthereumAddressFromDid,
urlFormating,
codeFormating,
};