From c0f6f62cfeccb45c26a60c69ec2c8ee1db65fe50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=27rysiek=27=20Wo=C5=BAniak?= Date: Sat, 10 Dec 2022 23:49:10 +0000 Subject: [PATCH] cli: signed-integrity - started implementing the gen-integrity action (ref. #66) --- plugins/signed-integrity/cli.js | 160 +++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 3 deletions(-) diff --git a/plugins/signed-integrity/cli.js b/plugins/signed-integrity/cli.js index 50abf6f..6f405f8 100644 --- a/plugins/signed-integrity/cli.js +++ b/plugins/signed-integrity/cli.js @@ -10,6 +10,22 @@ */ +/** + * helper function, converting binary to base64 + * this need not be extremely fast, since it will only be used on digests + * + * binary_data - data to convert to base64 + */ +let binToBase64 = (binary_data) => { + return btoa( + (new Uint8Array(binary_data)) + .reduce((bin, byte)=>{ + return bin += String.fromCharCode(byte) + }, '') + ) +} + + /** * generate an ECDSA P-384 keypair and export it as a JWK * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#json_web_key @@ -30,7 +46,7 @@ let genKeypair = async () => { } /** - * derive a public key from a provided public key file + * get a public key from a provided private key file * * keyfile - a path to a file containing the private key */ @@ -42,7 +58,7 @@ let getPubkey = async (keyfile) => { } // var keydata = JSON.parse(await Deno.readTextFile(keyfile)); - // the key can be eitehr in a CryptoKeyPair structure, or directly in CryptoKey structure + // the key can be either in a CryptoKeyPair structure, or directly in CryptoKey structure // standardize! if ("privateKey" in keydata) { keydata = keydata.privateKey @@ -54,12 +70,131 @@ let getPubkey = async (keyfile) => { keydata.key_ops = ['verify'] // import the key, thus making sure data is valid and makes sense - let key = await crypto.subtle.importKey("jwk", keydata, {name: 'ECDSA', namedCurve: 'P-384'}, true, ['verify']) + let key = await crypto.subtle.importKey( + "jwk", + keydata, + { + name: 'ECDSA', + namedCurve: 'P-384' + }, + true, + ['verify'] + ) // export it again return JSON.stringify(await crypto.subtle.exportKey("jwk", key)) } +/** + * get integrity digests for a given path + * + * path - path to a file whose digest is to be generated + * algos - array of SubtleCrypto.digest-compatible algorithm names + */ +let getFileIntegrity = async (path, algos) => { + + var result = [] + + // open the file and get some info on it + const file = await Deno.open( + path, + { read: true } + ); + const fileInfo = await file.stat(); + + // are we working with a file? + if (fileInfo.isFile) { + + //console.log(`+-- reading: ${path}`) + + // initialize + var content = new Uint8Array() + var buf = new Uint8Array(1000); + + // read the first batch + var numread = file.readSync(buf); + + // read the rest, if there is anything to read + while (numread !== null) { + //console.log(` +-- read: ${numread}`) + //console.log(` +-- length: ${content.length}`) + + // there has to be a better way... + var new_content = new Uint8Array(content.length + numread); + //console.log(` +-- new length: ${new_content.length}`) + new_content.set(content) + if (buf.length === numread) { + new_content.set(buf, content.length) + } else { + new_content.set(buf.slice(0, numread), content.length) + } + content = new_content + + // read some more + numread = file.readSync(buf); + } + //console.log(' +-- done.') + + //console.log('+-- calculating digests') + for (const algo of algos) { + //console.log(` +-- ${algo}`) + var digest = algo.toLowerCase().replace('-', '') + '-' + binToBase64(await crypto.subtle.digest(algo, content)) + //console.log(digest) + result.push(digest) + } + //console.log(`+-- file done: ${path}`) + + // we are not working with a file + } else { + result = false; + } + + // putting this in a try-catch block as the file + // is apparently being auto-closed? + // https://issueantenna.com/repo/denoland/deno/issues/15442 + try { + await file.close(); + } catch (BadResource) {} + + // return the result + return result +} + + +/** + * generate integrity files for provided paths + * + * paths - paths to files for which integrity files are to be generated + * keyfile - path of the file containing the private key to use + * extension - file extension to use when saving integrity files (default: ".integrity") + */ +let genSignedIntegrity = async (paths, keyfile, extension='.integrity') => { + + // load the key + var keydata = JSON.parse(await Deno.readTextFile(keyfile)); + + // the key can be either in a CryptoKeyPair structure, or directly in CryptoKey structure + // standardize! + if ("privateKey" in keydata) { + keydata = keydata.privateKey + } + + // import the key, thus making sure data is valid and makes sense + let privkey = await crypto.subtle.importKey( + "jwk", + keydata, + { + name: 'ECDSA', + namedCurve: 'P-384' + + }, + true, + ['sign'] + ) + + // TODO: implement shit +} + // this never changes const pluginName = "signed-integrity" const pluginDescription = "Fetching signed integrity data and using it to verify content.\nCLI used to generate subresource integrity tokens and save them in integrity files." @@ -78,6 +213,25 @@ const pluginActions = { description: "file containing the private key in JSON Web Key format" } } + }, + "gen-integrity": { + run: genSignedIntegrity, + description: "generate integrity files for given paths", + arguments: { + '_': { + name: "file", + description: "paths to generate signed integrity files for" + }, + keyfile: { + description: "path to the file containing a private key in JSON Web Key format", + string: true + }, + extension: { + description: "file extension to use when saving integrity files", + default: '.integrity', + string: true + } + } } }