Initial commit with translated description
This commit is contained in:
35
scripts/shared/storage/base.js
Normal file
35
scripts/shared/storage/base.js
Normal 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 };
|
||||
53
scripts/shared/storage/challenge.js
Normal file
53
scripts/shared/storage/challenge.js
Normal 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 };
|
||||
81
scripts/shared/storage/crypto.js
Normal file
81
scripts/shared/storage/crypto.js
Normal 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 };
|
||||
47
scripts/shared/storage/did.js
Normal file
47
scripts/shared/storage/did.js
Normal 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 };
|
||||
44
scripts/shared/storage/identities.js
Normal file
44
scripts/shared/storage/identities.js
Normal 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 };
|
||||
117
scripts/shared/storage/keys.js
Normal file
117
scripts/shared/storage/keys.js
Normal 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 };
|
||||
Reference in New Issue
Block a user