diff --git a/BUILD.gn b/BUILD.gn index f5a5f30dda9139..4fc0c28b7b7380 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -98,6 +98,7 @@ ts_sources = [ "js/url.ts", "js/url_search_params.ts", "js/util.ts", + "js/webcrypto.ts", "js/write_file.ts", "tsconfig.json", diff --git a/js/globals.ts b/js/globals.ts index efa377e9bf87ad..1d1fdd0a3b2df6 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -17,6 +17,7 @@ import * as textEncoding from "./text_encoding"; import * as timers from "./timers"; import * as url from "./url"; import * as urlSearchParams from "./url_search_params"; +import * as webcrypto from "./webcrypto"; // These imports are not exposed and therefore are fine to just import the // symbols required. @@ -78,3 +79,6 @@ window.TextEncoder = textEncoding.TextEncoder; export type TextEncoder = textEncoding.TextEncoder; window.TextDecoder = textEncoding.TextDecoder; export type TextDecoder = textEncoding.TextDecoder; + +window.SubtleCrypto = webcrypto.SubtleCrypto; +window.crypto = webcrypto.crypto; diff --git a/js/unit_tests.ts b/js/unit_tests.ts index b24a156d549310..910636709a5d8a 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -38,6 +38,7 @@ import "./timers_test.ts"; import "./truncate_test.ts"; import "./url_test.ts"; import "./url_search_params_test.ts"; +import "./webcrypto_test.ts"; import "./write_file_test.ts"; // TODO import "../website/app_test.js"; diff --git a/js/webcrypto.ts b/js/webcrypto.ts new file mode 100644 index 00000000000000..9da84958e397cc --- /dev/null +++ b/js/webcrypto.ts @@ -0,0 +1,115 @@ +import * as msg from "gen/msg_generated"; +import * as flatbuffers from "./flatbuffers"; +import { assert } from "./util"; +import { sendAsync } from "./dispatch"; + +function req( + algorithm: string, + data: Uint8Array +): [flatbuffers.Builder, msg.Any, flatbuffers.Offset] { + const builder = flatbuffers.createBuilder(); + const algoLoc = builder.createString(algorithm); + const dataLoc = msg.WebCryptoDigest.createDataVector(builder, data); + msg.WebCryptoDigest.startWebCryptoDigest(builder); + msg.WebCryptoDigest.addAlgorithm(builder, algoLoc); + msg.WebCryptoDigest.addData(builder, dataLoc); + const inner = msg.WebCryptoDigest.endWebCryptoDigest(builder); + return [builder, msg.Any.WebCryptoDigest, inner]; +} + +function res(baseRes: null | msg.Base): ArrayBuffer | null { + assert(baseRes !== null); + assert(msg.Any.WebCryptoDigestRes === baseRes!.innerType()); + const res = new msg.WebCryptoDigestRes(); + assert(baseRes!.inner(res) !== null); + const result = res.resultArray(); + if (result !== null) { + return result; + } + return null; +} + +const kHashFuncs = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]; +type BinaryArray = + | Int8Array + | Int16Array + | Int32Array + | Uint8Array + | Uint16Array + | Uint32Array + | Uint8ClampedArray + | Float32Array + | Float64Array + | DataView + | ArrayBuffer; +function binaryArrayToBytes(bin: BinaryArray): Uint8Array { + const buf = new ArrayBuffer(bin.byteLength); + let view = new DataView(buf); + if (bin instanceof Int8Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setInt8(i, bin[i]); + } + } else if (bin instanceof Int16Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setInt16(i * 2, bin[i]); + } + } else if (bin instanceof Int32Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setInt32(i * 4, bin[i]); + } + } else if (bin instanceof Uint8Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setUint8(i, bin[i]); + } + } else if (bin instanceof Uint16Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setUint16(i * 2, bin[i]); + } + } else if (bin instanceof Uint32Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setUint32(i * 4, bin[i]); + } + } else if (bin instanceof Uint8ClampedArray) { + for (let i = 0; i < bin.byteLength; i++) { + view.setUint8(i, bin[i]); + } + } else if (bin instanceof Float32Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setFloat32(i * 4, bin[i]); + } + } else if (bin instanceof Float64Array) { + for (let i = 0; i < bin.byteLength; i++) { + view.setFloat64(i * 8, bin[i]); + } + } else if (bin instanceof ArrayBuffer) { + view = new DataView(bin); + } else if (bin instanceof DataView) { + view = bin; + } + const ret = new Uint8Array(view.byteLength); + for (let i = 0; i < view.byteLength; i++) { + ret[i] = view.getUint8(i); + } + return ret; +} +async function digest(algorithm: string, bin: BinaryArray) { + if (kHashFuncs.indexOf(algorithm) < 0) { + throw new Error(`Unsupported hash function: ${algorithm}`); + } + const data = binaryArrayToBytes(bin); + return res(await sendAsync(...req(algorithm, data))); +} + +export class SubtleCrypto { + digest(algorithm: string | { name: string }, data: BinaryArray) { + if (typeof algorithm === "string") { + return digest(algorithm, data); + } else { + return digest(algorithm.name, data); + } + } +} + +export const crypto = { + subtle: new SubtleCrypto() +}; diff --git a/js/webcrypto_test.ts b/js/webcrypto_test.ts new file mode 100644 index 00000000000000..703d9ec739af11 --- /dev/null +++ b/js/webcrypto_test.ts @@ -0,0 +1,59 @@ +import { test, assert, assertEqual } from "./test_util.ts"; +import * as deno from "deno"; + +const encoder = new TextEncoder(); +function bytesToHex(bytes: Uint8Array | ArrayBuffer) { + let hex = ""; + for (let i = 0; i < bytes.byteLength; i++) { + let h = (bytes[i] & 0xff).toString(16); + if (h.length === 1) h = "0" + h; + hex += h; + } + return hex; +} +test(async function testSha1() { + const bytes = encoder.encode("abcde"); + const res = await crypto.subtle.digest("SHA-1", bytes); + let hash = bytesToHex(res); + assertEqual(hash, "03de6c570bfe24bfc328ccd7ca46b76eadaf4334"); +}); + +test(async function testSha256() { + const bytes = encoder.encode("abcde"); + const res = await crypto.subtle.digest("SHA-256", bytes); + const hash = bytesToHex(res); + assertEqual( + hash, + "36bbe50ed96841d10443bcb670d6554f0a34b761be67ec9c4a8ad2c0c44ca42c" + ); +}); + +test(async function testSha384() { + const bytes = encoder.encode("abcde"); + const res = await crypto.subtle.digest("SHA-384", bytes); + const hash = bytesToHex(res); + assertEqual( + hash, + "4c525cbeac729eaf4b4665815bc5db0c84fe6300068a727cf74e2813521565abc0ec57a37ee4d8be89d097c0d2ad52f0" + ); +}); + +test(async function testSha512() { + const bytes = encoder.encode("abcde"); + const res = await crypto.subtle.digest("SHA-512", bytes); + const hash = bytesToHex(res); + assertEqual( + hash, + "878ae65a92e86cac011a570d4c30a7eaec442b85ce8eca0c2952b5e3cc0628c2e79d889ad4d5c7c626986d452dd86374b6ffaa7cd8b67665bef2289a5c70b0a1" + ); +}); + +test(async function testUnsupportedHashFunc() { + let err; + try { + await crypto.subtle.digest("SHA-52", new Uint8Array([0, 1, 2])); + } catch (e) { + err = e; + } + assert(err !== void 0); +}); diff --git a/src/msg.fbs b/src/msg.fbs index b576d0f23dd943..e874cfbee8edec 100644 --- a/src/msg.fbs +++ b/src/msg.fbs @@ -56,7 +56,9 @@ union Any { Run, RunRes, RunStatus, - RunStatusRes + RunStatusRes, + WebCryptoDigest, + WebCryptoDigestRes } enum ErrorKind: byte { @@ -451,4 +453,13 @@ table RunStatusRes { exit_signal: int; } +table WebCryptoDigest { + algorithm: string; + data: [ubyte]; +} + +table WebCryptoDigestRes { + result: [ubyte]; +} + root_type Base; diff --git a/src/ops.rs b/src/ops.rs index 5b2598728ef8a2..b9b9c865470bb4 100644 --- a/src/ops.rs +++ b/src/ops.rs @@ -22,6 +22,7 @@ use hyper::rt::Future; use remove_dir_all::remove_dir_all; use repl; use resources::table_entries; +use ring::digest; use std; use std::convert::From; use std::fs; @@ -115,6 +116,7 @@ pub fn dispatch( msg::Any::Truncate => op_truncate, msg::Any::WriteFile => op_write_file, msg::Any::Write => op_write, + msg::Any::WebCryptoDigest => op_webcrypto_digest, _ => panic!(format!( "Unhandled message {}", msg::enum_name_any(inner_type) @@ -1529,3 +1531,39 @@ fn op_run_status( }); Box::new(future) } + +fn op_webcrypto_digest( + _state: &IsolateState, + base: &msg::Base, + data: libdeno::deno_buf, +) -> Box { + assert_eq!(data.len(), 0); + let inner = base.inner_as_web_crypto_digest().unwrap(); + let cmd_id = base.cmd_id(); + let algorithm = inner.algorithm().unwrap(); + let data = inner.data().unwrap(); + let builder = &mut FlatBufferBuilder::new(); + let hash = match algorithm { + "SHA-1" => digest::digest(&digest::SHA1, data), + "SHA-256" => digest::digest(&digest::SHA256, data), + "SHA-384" => digest::digest(&digest::SHA384, data), + "SHA-512" => digest::digest(&digest::SHA512, data), + _ => panic!("unsupported hash function"), + }; + let result = builder.create_vector(hash.as_ref()); + let inner = msg::WebCryptoDigestRes::create( + builder, + &msg::WebCryptoDigestResArgs { + result: Some(result), + }, + ); + ok_future(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::WebCryptoDigestRes, + ..Default::default() + }, + )) +}