Skip to content

Commit

Permalink
Better error handling in SubtleCrypto workers
Browse files Browse the repository at this point in the history
Handle exceptions from SubtleCrypto by catching and logging exceptions coming from the crypto stack.

Contributes to dotnet#69740
  • Loading branch information
eerhardt authored Jul 5, 2022
1 parent e0dca87 commit d309ea5
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,9 @@
<data name="Unknown_Error" xml:space="preserve">
<value>Unknown error.</value>
</data>
<data name="Unknown_SubtleCrypto_Error" xml:space="preserve">
<value>SubtleCrypto returned an unknown error: '{0}'.</value>
</data>
<data name="PlatformNotSupported_CipherModeBrowser" xml:space="preserve">
<value>Only CipherMode.CBC is supported on this platform.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ private unsafe int EncryptDecrypt(ReadOnlySpan<byte> input, Span<byte> output)
pOutput, output.Length);

if (bytesWritten < 0)
throw new Exception(SR.Unknown_Error);
throw new Exception(SR.Format(SR.Unknown_SubtleCrypto_Error, bytesWritten));

return bytesWritten;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,42 +43,35 @@ public override int GetCurrentHash(Span<byte> destination)
{
Debug.Assert(destination.Length >= _hashSizeInBytes);

byte[] srcArray = Array.Empty<byte>();
int srcLength = 0;
if (_buffer != null)
{
srcArray = _buffer.GetBuffer();
srcLength = (int)_buffer.Length;
}
ReadOnlySpan<byte> source = _buffer != null ?
new ReadOnlySpan<byte>(_buffer.GetBuffer(), 0, (int)_buffer.Length) :
default;

unsafe
{
fixed (byte* key = _key)
fixed (byte* src = srcArray)
fixed (byte* dest = destination)
{
int res = Interop.BrowserCrypto.Sign(_hashAlgorithm, key, _key.Length, src, srcLength, dest, destination.Length);
Debug.Assert(res != 0);
}
}
Sign(_hashAlgorithm, _key, source, destination);

return _hashSizeInBytes;
}

public static unsafe int MacDataOneShot(string hashAlgorithmId, ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination)
public static int MacDataOneShot(string hashAlgorithmId, ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination)
{
(SimpleDigest hashName, int hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId);
Debug.Assert(destination.Length >= hashSizeInBytes);

Sign(hashName, key, data, destination);

return hashSizeInBytes;
}

private static unsafe void Sign(SimpleDigest hashName, ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination)
{
fixed (byte* k = key)
fixed (byte* src = data)
fixed (byte* dest = destination)
{
int res = Interop.BrowserCrypto.Sign(hashName, k, key.Length, src, data.Length, dest, destination.Length);
Debug.Assert(res != 0);
if (res != 0)
throw new Exception(SR.Format(SR.Unknown_SubtleCrypto_Error, res));
}

return hashSizeInBytes;
}

public override int HashSizeInBytes => _hashSizeInBytes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,40 +40,34 @@ public override int GetCurrentHash(Span<byte> destination)
{
Debug.Assert(destination.Length >= _hashSizeInBytes);

byte[] srcArray = Array.Empty<byte>();
int srcLength = 0;
if (_buffer != null)
{
srcArray = _buffer.GetBuffer();
srcLength = (int)_buffer.Length;
}
ReadOnlySpan<byte> source = _buffer != null ?
new ReadOnlySpan<byte>(_buffer.GetBuffer(), 0, (int)_buffer.Length) :
default;

unsafe
{
fixed (byte* src = srcArray)
fixed (byte* dest = destination)
{
int res = Interop.BrowserCrypto.SimpleDigestHash(_impl, src, srcLength, dest, destination.Length);
Debug.Assert(res != 0);
}
}
SimpleDigestHash(_impl, source, destination);

return _hashSizeInBytes;
}

public static unsafe int HashOneShot(string hashAlgorithmId, ReadOnlySpan<byte> data, Span<byte> destination)
public static int HashOneShot(string hashAlgorithmId, ReadOnlySpan<byte> data, Span<byte> destination)
{
(SimpleDigest impl, int hashSizeInBytes) = HashAlgorithmToPal(hashAlgorithmId);
Debug.Assert(destination.Length >= hashSizeInBytes);

SimpleDigestHash(impl, data, destination);

return hashSizeInBytes;
}

private static unsafe void SimpleDigestHash(SimpleDigest hashName, ReadOnlySpan<byte> data, Span<byte> destination)
{
fixed (byte* src = data)
fixed (byte* dest = destination)
{
int res = Interop.BrowserCrypto.SimpleDigestHash(impl, src, data.Length, dest, destination.Length);
Debug.Assert(res != 0);
int res = Interop.BrowserCrypto.SimpleDigestHash(hashName, src, data.Length, dest, destination.Length);
if (res != 0)
throw new Exception(SR.Format(SR.Unknown_SubtleCrypto_Error, res));
}

return hashSizeInBytes;
}

public override int HashSizeInBytes => _hashSizeInBytes;
Expand Down
84 changes: 60 additions & 24 deletions src/mono/wasm/runtime/crypto-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
import { Module } from "./imports";
import { mono_assert } from "./types";

class ArgumentsError extends Error {}

const ERR_ARGS = -1;
const ERR_WORKER_FAILED = -2;
const ERR_OP_FAILED = -3;
const ERR_UNKNOWN = -100;

let mono_wasm_crypto: {
channel: LibraryChannel
worker: Worker
Expand All @@ -14,51 +21,51 @@ export function dotnet_browser_can_use_subtle_crypto_impl(): number {
}

export function dotnet_browser_simple_digest_hash(ver: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number {
mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized");

const msg = {
func: "digest",
type: ver,
data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len))
};

const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg));
const digest = JSON.parse(response);
if (digest.length > output_len) {
console.info("call_digest: about to throw!");
throw "DIGEST HASH: Digest length exceeds output length: " + digest.length + " > " + output_len;
const result = _send_msg_worker(msg);
if (typeof result === 'number') {
return result;
}

if (result.length > output_len) {
console.error("DIGEST HASH: Digest length exceeds output length: " + result.length + " > " + output_len);
return ERR_ARGS;
}

Module.HEAPU8.set(digest, output_buffer);
return 1;
Module.HEAPU8.set(result, output_buffer);
return 0;
}

export function dotnet_browser_sign(hashAlgorithm: number, key_buffer: number, key_len: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number {
mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized");

const msg = {
func: "sign",
type: hashAlgorithm,
key: Array.from(Module.HEAPU8.subarray(key_buffer, key_buffer + key_len)),
data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len))
};

const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg));
const signResult = JSON.parse(response);
if (signResult.length > output_len) {
console.info("dotnet_browser_sign: about to throw!");
throw "SIGN HASH: Sign length exceeds output length: " + signResult.length + " > " + output_len;
const result = _send_msg_worker(msg);
if (typeof result === 'number') {
return result;
}

if (result.length > output_len) {
console.error("SIGN HASH: Sign length exceeds output length: " + result.length + " > " + output_len);
return ERR_ARGS;
}

Module.HEAPU8.set(signResult, output_buffer);
return 1;
Module.HEAPU8.set(result, output_buffer);
return 0;
}

const AesBlockSizeBytes = 16; // 128 bits

export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer: number, key_len: number, iv_buffer: number, iv_len: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number {
mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized");

if (input_len <= 0 || input_len % AesBlockSizeBytes !== 0) {
throw "ENCRYPT DECRYPT: data was not a full block: " + input_len;
}
Expand All @@ -71,12 +78,14 @@ export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer
data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len))
};

const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg));
const result = JSON.parse(response);
const result = _send_msg_worker(msg);
if (typeof result === 'number') {
return result;
}

if (result.length > output_len) {
console.info("dotnet_browser_encrypt_decrypt: about to throw!");
throw "ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: " + result.length + " > " + output_len;
console.error("ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: " + result.length + " > " + output_len);
return ERR_ARGS;
}

Module.HEAPU8.set(result, output_buffer);
Expand Down Expand Up @@ -108,6 +117,33 @@ export function init_crypto(): void {
}
}

function _send_msg_worker(msg: any): any {
mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized");

try {
const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg));
const responseJson = JSON.parse(response);

if (responseJson.error !== undefined) {
console.error(`Worker failed with: ${responseJson.error}`);
if (responseJson.error_type == "ArgumentsError")
return ERR_ARGS;
if (responseJson.error_type == "WorkerFailedError")
return ERR_WORKER_FAILED;

return ERR_UNKNOWN;
}

return responseJson.result;
} catch (err) {
if (err instanceof Error && err.stack !== undefined)
console.error(`${err.stack}`);
else
console.error(`_send_msg_worker failed: ${err}`);
return ERR_OP_FAILED;
}
}

class LibraryChannel {
private msg_char_len: number;
private comm_buf: SharedArrayBuffer;
Expand Down
35 changes: 20 additions & 15 deletions src/mono/wasm/runtime/workers/dotnet-crypto-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

class ArgumentsError extends Error {}
class WorkerFailedError extends Error {}

var ChannelWorker = {
_impl: class {
// LOCK states
Expand Down Expand Up @@ -42,18 +45,19 @@ var ChannelWorker = {
if (req === this.STATE_SHUTDOWN)
break;

var resp = null;
var resp = {};
try {
// Perform async action based on request
resp = await async_call(req);
resp.result = await async_call(req);
}
catch (err) {
console.log("Request error: " + err);
resp = JSON.stringify(err);
resp.error_type = typeof err;
resp.error = _stringify_err(err);
console.error(`Request error: ${resp.error}. req was: ${req}`);
}

// Send response
this._send_response(resp);
this._send_response(JSON.stringify(resp));
}
}

Expand Down Expand Up @@ -98,7 +102,7 @@ var ChannelWorker = {

_send_response(msg) {
if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_REQ)
throw "WORKER: Invalid sync communication channel state.";
throw new WorkerFailedError(`WORKER: Invalid sync communication channel state.`);

var state; // State machine variable
const msg_len = msg.length;
Expand Down Expand Up @@ -152,7 +156,7 @@ var ChannelWorker = {
_release_lock() {
const result = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_OWNED, this.LOCK_UNLOCKED);
if (result !== this.LOCK_OWNED) {
throw "CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result;
throw new WorkerFailedError("CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result);
}
}
},
Expand Down Expand Up @@ -193,7 +197,7 @@ function get_hash_name(type) {
case 2: return "SHA-384";
case 3: return "SHA-512";
default:
throw "CRYPTO: Unknown digest: " + type;
throw new ArgumentsError("CRYPTO: Unknown digest: " + type);
}
}

Expand Down Expand Up @@ -271,22 +275,23 @@ async function async_call(msg) {
const req = JSON.parse(msg);

if (req.func === "digest") {
const digestArr = await call_digest(req.type, new Uint8Array(req.data));
return JSON.stringify(digestArr);
return await call_digest(req.type, new Uint8Array(req.data));
}
else if (req.func === "sign") {
const signResult = await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data));
return JSON.stringify(signResult);
return await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data));
}
else if (req.func === "encrypt_decrypt") {
const signResult = await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data);
return JSON.stringify(signResult);
return await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data);
}
else {
throw "CRYPTO: Unknown request: " + req.func;
throw new ArgumentsError("CRYPTO: Unknown request: " + req.func);
}
}

function _stringify_err(err) {
return (err instanceof Error && err.stack !== undefined) ? err.stack : err;
}

var s_channel;

// Initialize WebWorker
Expand Down

0 comments on commit d309ea5

Please sign in to comment.