From 083df815349282ba2a20d641bc75e51cefd4b7fd Mon Sep 17 00:00:00 2001 From: Derrick Hammer Date: Sat, 20 Aug 2022 03:46:43 -0400 Subject: [PATCH] *Merge in kernel module --- build.js | 14 ++ package.json | 16 +- src-build/build.ts | 369 +++++++++++++++++++++++++++++++++++++++++++ src-module/index.ts | 4 + tsconfig.build.json | 13 ++ tsconfig.json | 2 +- tsconfig.module.json | 12 ++ 7 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 build.js create mode 100644 src-build/build.ts create mode 100644 src-module/index.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.module.json diff --git a/build.js b/build.js new file mode 100644 index 0000000..8c01e5f --- /dev/null +++ b/build.js @@ -0,0 +1,14 @@ +import esbuild from "esbuild" + +esbuild.buildSync({ + entryPoints: ['src/index.ts'], + outfile: 'dist-module/index.js', + format: 'esm', + bundle: true, + legalComments: 'external', + // minify: true + tsconfig: "tsconfig.module.json", + define: { + 'global': 'self' + } +}) diff --git a/package.json b/package.json index c44f68e..4b0066f 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,22 @@ "version": "0.1.0", "main": "dist/index.js", "type": "module", + "scripts": { + "build-script": "tsc --project tsconfig.build.json && mv dist-build/build.js dist-build/build.mjs", + "compile-module": "npm run build-script && node build.js", + "build": "tsc --project tsconfig.json", + "build-module": "npm run compile-module && node ./dist-build/build.mjs dev" + }, "dependencies": { - "@lumeweb/resolver-common": "github:LumeWeb/resolver-common", + "@lumeweb/kernel-libresolver": "https://github.com/LumeWeb/kernel-libresolver.git", + "@lumeweb/libresolver": "https://github.com/LumeWeb/libresolver.git", "@lumeweb/tld-enum": "github:LumeWeb/list-of-top-level-domains" + }, + "devDependencies": { + "@types/node": "^18.7.8", + "@types/read": "^0.0.29", + "esbuild": "^0.15.5", + "libskynetnode": "^0.1.4", + "read": "^1.0.7" } } diff --git a/src-build/build.ts b/src-build/build.ts new file mode 100644 index 0000000..e66a9b8 --- /dev/null +++ b/src-build/build.ts @@ -0,0 +1,369 @@ +// This is the standard build script for a kernel module. + +import * as fs from "fs"; +import { + addContextToErr, + b64ToBuf, + bufToHex, + deriveRegistryEntryID, + entryIDToSkylink, + generateSeedPhraseDeterministic, + seedPhraseToSeed, + sha512, + taggedRegistryEntryKeys, +} from "libskynet"; +import { + generateSeedPhraseRandom, + overwriteRegistryEntry, + upload, +} from "libskynetnode"; +import read from "read"; + +// Helper variables to make it easier to return empty values alongside errors. +const nu8 = new Uint8Array(0); +const nkp = { + publicKey: nu8, + secretKey: nu8, +}; + +// readFile is a wrapper for fs.readFileSync that handles the try-catch for the +// caller. +function readFile(fileName: string): [string, string | null] { + try { + const data = fs.readFileSync(fileName, "utf8"); + return [data, null]; + } catch (err) { + return ["", "unable to read file: " + JSON.stringify(err)]; + } +} + +// readFileBinary is a wrapper for fs.readFileSync that handles the try-catch +// for the caller. +function readFileBinary(fileName: string): [Uint8Array, string | null] { + try { + const data = fs.readFileSync(fileName, null); + return [data, null]; + } catch (err) { + return [nu8, "unable to read file: " + JSON.stringify(err)]; + } +} + +// writeFile is a wrapper for fs.writeFileSync which handles the try-catch in a +// non-exception way. +function writeFile(fileName: string, fileData: string): string | null { + try { + fs.writeFileSync(fileName, fileData); + return null; + } catch (err) { + return "unable to write file: " + JSON.stringify(err); + } +} + +// hardenedSeedPhrase will take a password, harden it with 100,000 iterations +// of hashing, and then turn it into a seed phrase. +function hardenedSeedPhrase(password: string): [string, string | null] { + // Add some hashing iterations to the password to make it stronger. + for (let i = 0; i < 1000000; i++) { + const passU8 = new TextEncoder().encode(password); + const hashIter = sha512(passU8); + password = bufToHex(hashIter); + } + return generateSeedPhraseDeterministic(password); +} + +// seedPhraseToRegistryKeys will convert a seed phrase to the set of registry +// keys that govern the registry entry where the module is published. +function seedPhraseToRegistryKeys( + seedPhrase: string +): [any, Uint8Array, string | null] { + const [seed, errVSP] = seedPhraseToSeed(seedPhrase); + if (errVSP !== null) { + return [nkp, nu8, addContextToErr(errVSP, "unable to compute seed phrase")]; + } + const [keypair, datakey, errTREK] = taggedRegistryEntryKeys( + seed, + "module-build", + "module-key" + ); + if (errTREK !== null) { + return [ + nkp, + nu8, + addContextToErr(errTREK, "unable to compute registry entry keys"), + ]; + } + return [keypair, datakey, null]; +} + +// seedPhraseToRegistryLink will take a seedPhrase as input and convert it to +// the registry link for the module. +function seedPhraseToRegistryLink(seedPhrase: string): [string, string | null] { + const [keypair, datakey, errSPTRK] = seedPhraseToRegistryKeys(seedPhrase); + if (errSPTRK !== null) { + return ["", addContextToErr(errSPTRK, "unable to compute registry keys")]; + } + const [entryID, errDREID] = deriveRegistryEntryID(keypair.publicKey, datakey); + if (errDREID !== null) { + return [ + "", + addContextToErr(errDREID, "unable to compute registry entry id"), + ]; + } + const registryLink = entryIDToSkylink(entryID); + return [registryLink, null]; +} + +// handlePass handles all portions of the script that occur after the password +// has been requested. If no password needs to be requested, handlePass will be +// called with a null input. We need to structure the code this way because the +// password reader is async and we can only access the password when using a +// callback. +function handlePass(password: string) { + try { + // If we are running prod and the seed file does not exist, we + // need to confirm the password and also warn the user to use a + // secure password. + if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { + // The file does not exist, we need to confirm the + // password. + console.log(); + console.log( + "No production entry found for module. Creating new production module..." + ); + console.log( + "If someone can guess the password, they can push arbitrary changes to your module." + ); + console.log("Please use a secure password."); + console.log(); + read( + { prompt: "Confirm Password: ", silent: true }, + function (err: any, confirmPassword: string) { + if (err) { + console.error("unable to fetch password:", err); + process.exit(1); + } + if (password !== confirmPassword) { + console.error("passwords do not match"); + process.exit(1); + } + password = password + moduleSalt; + handlePassConfirm(password); + } + ); + } else { + // If the seed file does exist, or if we are using dev, + // there's no need to confirm the password but we do + // need to pass the logic off to the handlePassConfirm + // callback. + password = password + moduleSalt; + handlePassConfirm(password); + } + } catch (err) { + console.error("Unable to read seedFile:", err); + process.exit(1); + } +} + +// handlePassConfirm handles the full script after the confirmation password +// has been provided. If not confirmation password is needed, this function +// will be called anyway using the unconfirmed password as input. +function handlePassConfirm(password: string) { + // Create the seedFile if it does not exist. For dev we just save the + // seed to disk outright, because this is a dev build and therefore not + // security sensitive. Also the dev seed does not get pushed to the + // github repo. + // + // For prod, we use the seed to create a new seed (called the shield) + // which allows us to verify that the developer has provided the right + // password when deploying the module. The shield does get pushed to + // the github repo so that the production module is the same on all + // devices. + if (!fs.existsSync(seedFile) && process.argv[2] !== "prod") { + // Generate the seed phrase and write it to the file. + const [seedPhrase, errGSP] = generateSeedPhraseRandom(); + if (errGSP !== null) { + console.error("Unable to generate seed phrase:", errGSP); + process.exit(1); + } + const errWF = writeFile(seedFile, seedPhrase); + if (errWF !== null) { + console.error("unable to write file:", errWF); + process.exit(1); + } + } else if (!fs.existsSync(seedFile) && process.argv[2] === "prod") { + // Generate the seed phrase. + const [seedPhrase, errGSP] = hardenedSeedPhrase(password); + if (errGSP !== null) { + console.error("Unable to generate seed phrase:", errGSP); + process.exit(1); + } + const [registryLink, errSPTRL] = seedPhraseToRegistryLink(seedPhrase); + if (errSPTRL !== null) { + console.error("Unable to generate registry link:", errSPTRL); + process.exit(1); + } + + // Write the registry link to the file. + const errWF = writeFile(seedFile, registryLink); + if (errWF !== null) { + console.error("unable to write registry link file:", errWF); + process.exit(1); + } + } + + // Load or verify the seed. If this is prod, the password is used to + // create and verify the seed. If this is dev, we just load the seed + // with no password. + let seedPhrase: string; + let registryLink: string; + if (process.argv[2] === "prod") { + // Generate the seed phrase from the password. + const [sp, errGSP] = hardenedSeedPhrase(password); + if (errGSP !== null) { + console.error("Unable to generate seed phrase: ", errGSP); + process.exit(1); + } + const [rl, errSPTRL] = seedPhraseToRegistryLink(sp); + registryLink = rl; + if (errSPTRL !== null) { + console.error("Unable to generate registry link:", errSPTRL); + process.exit(1); + } + const [registryLinkVerify, errRF] = readFile(seedFile); + if (errRF !== null) { + console.error("unable to read seedFile"); + process.exit(1); + } + const replacedRegistryLinkVerify = registryLinkVerify.replace(/\n$/, ""); + if (registryLink !== replacedRegistryLinkVerify) { + console.error("Incorrect password"); + process.exit(1); + } + seedPhrase = sp; + } else { + const [sp, errRF] = readFile(seedFile); + if (errRF !== null) { + console.error("unable to read seed phrase for dev command from disk"); + process.exit(1); + } + const [rl, errSPTRL] = seedPhraseToRegistryLink(sp); + registryLink = rl; + if (errSPTRL !== null) { + console.error("Unable to generate registry link:", errSPTRL); + process.exit(1); + } + // Write the registry link to the module skylink dev file. + const errWF = writeFile("build/module-skylink-dev", registryLink); + if (errWF !== null) { + console.error("unable to write registry link file:", errWF); + process.exit(1); + } + seedPhrase = sp; + } + + // Upload the module to Skynet. + const [distFile, errRF] = readFileBinary("dist-module/index.js"); + if (errRF !== null) { + console.error("unable to read dist file for module"); + process.exit(1); + } + const metadata = { + Filename: "index.js", + }; + console.log("Uploading module..."); + upload(distFile, metadata) + .then((result) => { + console.log("Updating module's registry entry..."); + // Update the v2 skylink. + const [keypair, datakey, errSPTRK] = seedPhraseToRegistryKeys(seedPhrase); + if (errSPTRK !== null) { + return [ + "", + addContextToErr(errSPTRK, "unable to compute registry keys"), + ]; + } + const [bufLink, errBTB] = b64ToBuf(result); + if (errBTB !== null) { + return ["", addContextToErr(errBTB, "unable to decode skylink")]; + } + overwriteRegistryEntry(keypair, datakey, bufLink) + .then(() => { + console.log("registry entry is updated"); + console.log("Immutable Link for Module:", result); + console.log("Resolver Link for Module:", registryLink); + }) + .catch((err: any) => { + console.log("unable to update registry entry:", err); + }); + }) + .catch((err) => { + console.error("unable to upload file", err); + process.exit(1); + }); +} + +// Add a newline for readability. +console.log(); + +// Check for a 'dev' or 'prod' input to the script. +if (process.argv.length !== 3) { + console.error("need to provide either 'dev' or 'prod' as an input"); + process.exit(1); +} + +// Create the build folder if it does not exist. +if (!fs.existsSync("build")) { + fs.mkdirSync("build"); +} + +// Determine the seed file. +let seedFile: string; +if (process.argv[2] === "prod") { + seedFile = "module-skylink"; +} else if (process.argv[2] === "dev") { + seedFile = "build/dev-seed"; +} else { + console.error("need to provide either 'dev' or 'prod' as an input"); + process.exit(1); +} + +// If doing a prod deployment, check whether the salt file exists. If it does +// not, create it. +let moduleSalt: string; +if (!fs.existsSync(".module-salt")) { + const [ms, errGSPR] = generateSeedPhraseRandom(); + if (errGSPR !== null) { + console.error("unable to generate module salt:", errGSPR); + process.exit(1); + } + moduleSalt = ms; + const errWF = writeFile(".module-salt", moduleSalt); + if (errWF !== null) { + console.error("unable to write module salt file:", errWF); + process.exit(1); + } +} else { + const [ms, errRF] = readFile(".module-salt"); + if (errRF !== null) { + console.error("unable to read moduleSalt"); + process.exit(1); + } + const replaceMS = ms.replace(/\n$/, ""); + moduleSalt = replaceMS; +} + +// Need to get a password if this is a prod build. +if (process.argv[2] === "prod") { + read( + { prompt: "Password: ", silent: true }, + function (err: any, password: string) { + if (err) { + console.error("unable to fetch password:", err); + process.exit(1); + } + handlePass(password); + } + ); +} else { + handlePass(""); +} diff --git a/src-module/index.ts b/src-module/index.ts new file mode 100644 index 0000000..a2f6c31 --- /dev/null +++ b/src-module/index.ts @@ -0,0 +1,4 @@ +import { setup } from "@lumeweb/kernel-libresolver"; +import Handshake from "../src/index.js"; + +setup(Handshake); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..56508e4 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "declaration": true, + "outDir": "./dist-build", + "strict": true + }, + "include": ["src-build"], + "exclude": ["node_modules", "**/__tests__/*"] +} diff --git a/tsconfig.json b/tsconfig.json index d562877..38eb123 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "esnext", + "target": "es2021", "declaration": true, "moduleResolution": "node", "outDir": "./dist", diff --git a/tsconfig.module.json b/tsconfig.module.json new file mode 100644 index 0000000..22867ed --- /dev/null +++ b/tsconfig.module.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es2021", + "declaration": true, + "moduleResolution": "node", + "outDir": "./dist-module", + "strict": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src-module"], + "exclude": ["node_modules", "**/__tests__/*"] +}