feat: implement appdb, known as hidden db in s5
This commit is contained in:
parent
cc247a350e
commit
2894e63aa4
|
@ -0,0 +1,178 @@
|
||||||
|
import {
|
||||||
|
CID,
|
||||||
|
createKeyPair,
|
||||||
|
S5Node,
|
||||||
|
encryptionKeyDerivationTweak,
|
||||||
|
pathKeyDerivationTweak,
|
||||||
|
writeKeyDerivationTweak,
|
||||||
|
} from "@lumeweb/libs5";
|
||||||
|
import { deriveBlakeChildKey, deriveBlakeChildKeyInt } from "#keys.js";
|
||||||
|
import { uploadObject } from "#upload.js";
|
||||||
|
import { signRegistryEntry } from "@lumeweb/libs5/lib/service/registry.js";
|
||||||
|
import { encodeRegistryValue } from "#cid.js";
|
||||||
|
import { downloadSmallObject } from "#download.js";
|
||||||
|
import { readableStreamToUint8Array } from "binconv";
|
||||||
|
import { utf8ToBytes } from "@noble/hashes/utils";
|
||||||
|
import { decryptMutableBytes, encryptMutableBytes } from "#encryption.js";
|
||||||
|
|
||||||
|
export default class AppDb {
|
||||||
|
private readonly _rootKey: Uint8Array;
|
||||||
|
private readonly _node: S5Node;
|
||||||
|
private readonly _cidMap: Map<string, CID> = new Map<string, CID>();
|
||||||
|
|
||||||
|
constructor(rootKey: Uint8Array, api: S5Node) {
|
||||||
|
this._rootKey = rootKey;
|
||||||
|
this._node = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRawData(path: string): Promise<AppDbRawDataResponse> {
|
||||||
|
const pathKey = this._derivePathKeyForPath(path);
|
||||||
|
const encryptionKey = deriveBlakeChildKeyInt(
|
||||||
|
pathKey,
|
||||||
|
encryptionKeyDerivationTweak,
|
||||||
|
);
|
||||||
|
|
||||||
|
const writeKey = deriveBlakeChildKeyInt(pathKey, writeKeyDerivationTweak);
|
||||||
|
const keyPair = await createKeyPair(writeKey);
|
||||||
|
|
||||||
|
const sre = await this._node.services.registry.get(keyPair.publicKey);
|
||||||
|
if (!sre) {
|
||||||
|
return new AppDbRawDataResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cid = CID.fromBytes(sre.data.slice(1));
|
||||||
|
|
||||||
|
const bytes = await downloadSmallObject(cid.toString());
|
||||||
|
|
||||||
|
const plaintext = await decryptMutableBytes(
|
||||||
|
await readableStreamToUint8Array(bytes),
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this._cidMap.set(path, cid);
|
||||||
|
|
||||||
|
return new AppDbRawDataResponse({
|
||||||
|
data: plaintext,
|
||||||
|
cid,
|
||||||
|
revision: sre.revision,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setRawData(
|
||||||
|
path: string,
|
||||||
|
data: Uint8Array,
|
||||||
|
revision: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const pathKey = this._derivePathKeyForPath(path);
|
||||||
|
const encryptionKey = deriveBlakeChildKeyInt(
|
||||||
|
pathKey,
|
||||||
|
encryptionKeyDerivationTweak,
|
||||||
|
);
|
||||||
|
|
||||||
|
const cipherText = await encryptMutableBytes(data, encryptionKey);
|
||||||
|
|
||||||
|
const cid = await uploadObject(cipherText);
|
||||||
|
const writeKey = deriveBlakeChildKeyInt(pathKey, writeKeyDerivationTweak);
|
||||||
|
const keyPair = createKeyPair(writeKey);
|
||||||
|
const registryVal = encodeRegistryValue(cid);
|
||||||
|
|
||||||
|
const sre = await signRegistryEntry({
|
||||||
|
kp: keyPair,
|
||||||
|
data: registryVal,
|
||||||
|
revision,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._node.services.registry.set(sre);
|
||||||
|
|
||||||
|
this._cidMap.set(path, cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getJSON(path: string): Promise<AppDbJSONResponse> {
|
||||||
|
const res = await this.getRawData(path);
|
||||||
|
|
||||||
|
if (res.data === null) {
|
||||||
|
return new AppDbJSONResponse({ cid: res.cid });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedData = JSON.parse(new TextDecoder().decode(res.data));
|
||||||
|
return new AppDbJSONResponse({
|
||||||
|
data: decodedData,
|
||||||
|
revision: res.revision,
|
||||||
|
cid: res.cid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setJSON(
|
||||||
|
path: string,
|
||||||
|
data: any,
|
||||||
|
revision: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const encodedData = utf8ToBytes(JSON.stringify(data));
|
||||||
|
await this.setRawData(path, new Uint8Array(encodedData), revision);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _derivePathKeyForPath(path: string): Uint8Array {
|
||||||
|
const pathSegments = path
|
||||||
|
.split("/")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter((element) => element.length > 0);
|
||||||
|
|
||||||
|
const key = this._deriveKeyForPathSegments(pathSegments);
|
||||||
|
return deriveBlakeChildKeyInt(key, pathKeyDerivationTweak);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _deriveKeyForPathSegments(pathSegments: string[]): Uint8Array {
|
||||||
|
if (pathSegments.length === 0) {
|
||||||
|
return this._rootKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deriveBlakeChildKey(
|
||||||
|
this._deriveKeyForPathSegments(
|
||||||
|
pathSegments.slice(0, pathSegments.length - 1),
|
||||||
|
),
|
||||||
|
pathSegments[pathSegments.length - 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDbRawDataResponse {
|
||||||
|
data: Uint8Array | undefined;
|
||||||
|
revision: number;
|
||||||
|
cid: CID | undefined;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
data,
|
||||||
|
revision = -1,
|
||||||
|
cid,
|
||||||
|
}: {
|
||||||
|
data?: Uint8Array;
|
||||||
|
revision?: number;
|
||||||
|
cid?: CID;
|
||||||
|
} = {}) {
|
||||||
|
this.data = data;
|
||||||
|
this.revision = revision || -1;
|
||||||
|
this.cid = cid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppDbJSONResponse {
|
||||||
|
data: any;
|
||||||
|
revision: number;
|
||||||
|
cid: CID | undefined;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
data,
|
||||||
|
revision = -1,
|
||||||
|
cid,
|
||||||
|
}: {
|
||||||
|
data?: any;
|
||||||
|
revision?: number;
|
||||||
|
cid?: CID;
|
||||||
|
} = {}) {
|
||||||
|
this.data = data;
|
||||||
|
this.revision = revision || -1;
|
||||||
|
this.cid = cid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AppDbRawDataResponse, AppDbJSONResponse };
|
|
@ -0,0 +1,109 @@
|
||||||
|
import {
|
||||||
|
decodeEndian,
|
||||||
|
encodeEndian,
|
||||||
|
encryptionKeyLength,
|
||||||
|
encryptionNonceLength,
|
||||||
|
encryptionOverheadLength,
|
||||||
|
} from "@lumeweb/libs5";
|
||||||
|
|
||||||
|
import { xchacha20poly1305 } from "@noble/ciphers/chacha";
|
||||||
|
import { randomBytes } from "@noble/ciphers/webcrypto/utils";
|
||||||
|
|
||||||
|
function padFileSizeDefault(initialSize: number): number {
|
||||||
|
const kib = 1 << 10;
|
||||||
|
// Only iterate to 53 (the maximum safe power of 2).
|
||||||
|
for (let n = 0; n < 53; n++) {
|
||||||
|
if (initialSize <= (1 << n) * 80 * kib) {
|
||||||
|
const paddingBlock = (1 << n) * 4 * kib;
|
||||||
|
let finalSize = initialSize;
|
||||||
|
if (finalSize % paddingBlock !== 0) {
|
||||||
|
finalSize = initialSize - (initialSize % paddingBlock) + paddingBlock;
|
||||||
|
}
|
||||||
|
return finalSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prevent overflow.
|
||||||
|
throw new Error("Could not pad file size, overflow detected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPaddedBlock(size: number): boolean {
|
||||||
|
const kib = 1024;
|
||||||
|
// Only iterate to 53 (the maximum safe power of 2).
|
||||||
|
for (let n = 0; n < 53; n++) {
|
||||||
|
if (size <= (1 << n) * 80 * kib) {
|
||||||
|
const paddingBlock = (1 << n) * 4 * kib;
|
||||||
|
return size % paddingBlock === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Could not check padded file size, overflow detected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function encryptMutableBytes(
|
||||||
|
data: Uint8Array,
|
||||||
|
key: Uint8Array,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
const lengthInBytes = encodeEndian(data.length, 4);
|
||||||
|
|
||||||
|
const totalOverhead =
|
||||||
|
encryptionOverheadLength + 4 + encryptionNonceLength + 2;
|
||||||
|
|
||||||
|
const finalSize =
|
||||||
|
padFileSizeDefault(data.length + totalOverhead) - totalOverhead;
|
||||||
|
|
||||||
|
data = new Uint8Array([
|
||||||
|
...lengthInBytes,
|
||||||
|
...data,
|
||||||
|
...new Uint8Array(finalSize - data.length),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nonce = randomBytes(encryptionNonceLength);
|
||||||
|
|
||||||
|
const header = new Uint8Array([0x8d, 0x01, ...nonce]);
|
||||||
|
|
||||||
|
const stream_x = xchacha20poly1305(key, nonce);
|
||||||
|
|
||||||
|
const encryptedBytes = stream_x.encrypt(data);
|
||||||
|
|
||||||
|
return new Uint8Array([...header, ...encryptedBytes]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decryptMutableBytes(
|
||||||
|
data: Uint8Array,
|
||||||
|
key: Uint8Array,
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
if (key.length !== encryptionKeyLength) {
|
||||||
|
throw `wrong encryptionKeyLength (${key.length} != ${encryptionKeyLength})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!checkPaddedBlock(data.length)) {
|
||||||
|
throw `Expected parameter 'data' to be padded encrypted data, length was '${
|
||||||
|
data.length
|
||||||
|
}', nearest padded block is '${padFileSizeDefault(data.length)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = data[1];
|
||||||
|
if (version !== 0x01) {
|
||||||
|
throw "Invalid version";
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonce = data.subarray(2, encryptionNonceLength + 2);
|
||||||
|
|
||||||
|
const stream_x = xchacha20poly1305(key, nonce);
|
||||||
|
|
||||||
|
const decryptedBytes = stream_x.decrypt(
|
||||||
|
data.subarray(encryptionNonceLength + 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
const lengthInBytes = decryptedBytes.subarray(0, 4);
|
||||||
|
|
||||||
|
const length = decodeEndian(lengthInBytes);
|
||||||
|
|
||||||
|
return decryptedBytes.subarray(4, length + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
padFileSizeDefault,
|
||||||
|
checkPaddedBlock,
|
||||||
|
decryptMutableBytes,
|
||||||
|
encryptMutableBytes,
|
||||||
|
};
|
|
@ -1,6 +1,8 @@
|
||||||
import { ed25519 } from "@noble/curves/ed25519";
|
import { ed25519 } from "@noble/curves/ed25519";
|
||||||
import { sha512 } from "@noble/hashes/sha512";
|
import { sha512 } from "@noble/hashes/sha512";
|
||||||
|
|
||||||
|
import AppDb from "#appDb.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
bytesToHex,
|
bytesToHex,
|
||||||
hexToBytes,
|
hexToBytes,
|
||||||
|
@ -30,4 +32,5 @@ export * from "./keys.js";
|
||||||
export * from "./download.js";
|
export * from "./download.js";
|
||||||
export * from "./upload.js";
|
export * from "./upload.js";
|
||||||
export * from "./portal.js";
|
export * from "./portal.js";
|
||||||
export { ed25519, sha512 };
|
export * from "./encryption.js";
|
||||||
|
export { ed25519, sha512, AppDb };
|
||||||
|
|
Loading…
Reference in New Issue