Quantum
Quantum9mo ago

How do I make the crypto.subtle.digest algo flexible?

This is a learning moment, bear with me. 😅 I can hard code an algo for the digest like this:
const algo = "SHA-256";
const d = new Uint8Array(
await crypto.subtle.digest(
algo, new TextEncoder().encode("test")
)
);
const algo = "SHA-256";
const d = new Uint8Array(
await crypto.subtle.digest(
algo, new TextEncoder().encode("test")
)
);
But if I want to make the algo flexible, it forces me to make it a DigestAlgorithm and it cannot be a string shown here:
async function digest(algo: string): Promise<Uint8Array> {
return new Uint8Array(
await crypto.subtle.digest(
algo, new TextEncoder().encode("test")
)
);
}
async function digest(algo: string): Promise<Uint8Array> {
return new Uint8Array(
await crypto.subtle.digest(
algo, new TextEncoder().encode("test")
)
);
}
I reviewed what that is in the source: https://github.com/denoland/deno_std/blob/main/crypto/_wasm/mod.ts I see this: export type DigestAlgorithm = typeof digestAlgorithms[number]; (I don't quite see what is going on here and am interested.) Bottom line is, when I make algo:string a algo:DigestAlgorithm in the digest function param list, it stops complaining, but I need to have some calling code somewhere be a flexible string (or maybe an enum if necessary) that somehow translates to a DigestAlgorithm for this to work. I've been struggling with this. Any ideas?
18 Replies
marvinh.
marvinh.9mo ago
That's because not any random string is a valid algorithm. The string asdf for example is not a valid algorithm for the crypto module But typing it as string would mean that this would be valid, which leads to a type conflict the type string is too wide only particular strings are valid, and those are defined in the digestAlgorithms array
cknight
cknight9mo ago
DigestAlgorithm is still a string, just a very specific string from one of the digestAlgorithms. E.g.
export const digestAlgorithms = [
"BLAKE2B-128",
"BLAKE2B-224",
"BLAKE2B-256",
"BLAKE2B-384",
"BLAKE2B",
"BLAKE2S",
"BLAKE3",
"KECCAK-224",
"KECCAK-256",
"KECCAK-384",
"KECCAK-512",
"SHA-384",
"SHA3-224",
"SHA3-256",
"SHA3-384",
"SHA3-512",
"SHAKE128",
"SHAKE256",
"TIGER",
// insecure (length-extendable):
"RIPEMD-160",
"SHA-224",
"SHA-256",
"SHA-512",
// insecure (collidable and length-extendable):
"MD4",
"MD5",
"SHA-1",
]
export const digestAlgorithms = [
"BLAKE2B-128",
"BLAKE2B-224",
"BLAKE2B-256",
"BLAKE2B-384",
"BLAKE2B",
"BLAKE2S",
"BLAKE3",
"KECCAK-224",
"KECCAK-256",
"KECCAK-384",
"KECCAK-512",
"SHA-384",
"SHA3-224",
"SHA3-256",
"SHA3-384",
"SHA3-512",
"SHAKE128",
"SHAKE256",
"TIGER",
// insecure (length-extendable):
"RIPEMD-160",
"SHA-224",
"SHA-256",
"SHA-512",
// insecure (collidable and length-extendable):
"MD4",
"MD5",
"SHA-1",
]
This prevents your code from using an unsupported (or nonsense) algorithm.
Quantum
Quantum9mo ago
I agree and understand why. My goal is to understand how to make it flexible. Am I forced to hard code this value?
cknight
cknight9mo ago
By hard code, do you mean digestAlgorithms? No, you simply import DigestAlgorithm which is a type matching one of the above constants. What is this code for? A library? A web app? Can you provide more context? This is as flexible as possible:
/* Your library or module, "my_modules.ts" */
import { DigestAlgorithm } from "https://deno.land/std@0.206.0/crypto/mod.ts";
export async function digest(algo: DigestAlgorithm, content:string): Promise<Uint8Array> { ... }

/* In Consumer A module */
import {digest} from "./my_module.ts";
await digest("SHA-1", content);

/* In Consumer B module */
import {digest} from "./my_module.ts";
await digest("SHA-256", content);
/* Your library or module, "my_modules.ts" */
import { DigestAlgorithm } from "https://deno.land/std@0.206.0/crypto/mod.ts";
export async function digest(algo: DigestAlgorithm, content:string): Promise<Uint8Array> { ... }

/* In Consumer A module */
import {digest} from "./my_module.ts";
await digest("SHA-1", content);

/* In Consumer B module */
import {digest} from "./my_module.ts";
await digest("SHA-256", content);
and trying this gives a compile error when using Typescript:
/* In Consumer C module */
import {digest} from "./my_module.ts";
await digest("abcd", content); //Typescript complains since "abcd" is not of type DigestAlgorithm
/* In Consumer C module */
import {digest} from "./my_module.ts";
await digest("abcd", content); //Typescript complains since "abcd" is not of type DigestAlgorithm
Quantum
Quantum9mo ago
@cknight I am writing the beginnings of a deterministic pseudo random library that allows the user of the library to specify what algorithm to use. It would be a separate issue to discuss the merits of such a library. (Very happy to discuss!) The best that I can do to make this flexible is by doing the following:
import { crypto, DigestAlgorithm } from "https://deno.land/std@0.202.0/crypto/mod.ts";

export default class Random {
algo: DigestAlgorithm;
index: number;
random256!: Uint8Array;

constructor(algo: DigestAlgorithm) {
this.algo = algo;
this.index = 0;
}

static async new({algo, seed}: {algo: string, seed: string}): Promise<Random> {
let da: DigestAlgorithm = "BLAKE2B"; //default
if (algo == "BLAKE2B-224") da = "BLAKE2B-224";
if (algo == "BLAKE2B-256") da = "BLAKE2B-256";
if (algo == "BLAKE2B-384") da = "BLAKE2B-384";
if (algo == "BLAKE2B") da = "BLAKE2B";
if (algo == "BLAKE2S") da = "BLAKE2S";
if (algo == "BLAKE3") da = "BLAKE3";
if (algo == "KECCAK-224") da = "KECCAK-224";
if (algo == "KECCAK-256") da = "KECCAK-256";
if (algo == "KECCAK-384") da = "KECCAK-384";
if (algo == "KECCAK-512") da = "KECCAK-512";
if (algo == "SHA-384") da = "SHA-384";
if (algo == "SHA3-224") da = "SHA3-224";
if (algo == "SHA3-256") da = "SHA3-256";
if (algo == "SHA3-384") da = "SHA3-384";
if (algo == "SHA3-512") da = "SHA3-512";
if (algo == "SHAKE128") da = "SHAKE128";
if (algo == "SHAKE256") da = "SHAKE256";
if (algo == "TIGER") da = "TIGER";
if (algo == "RIPEMD-160") da = "RIPEMD-160";
if (algo == "SHA-224") da = "SHA-224";
if (algo == "SHA-256") da = "SHA-256";
if (algo == "SHA-512") da = "SHA-512";
if (algo == "MD4") da = "MD4";
if (algo == "MD5") da = "MD5";
if (algo == "SHA-1") da = "SHA-1";

const random = new Random(da);

random.random256 = new Uint8Array(
await crypto.subtle.digest(
random.algo, new TextEncoder().encode(seed)
)
);

return random;
}
}
import { crypto, DigestAlgorithm } from "https://deno.land/std@0.202.0/crypto/mod.ts";

export default class Random {
algo: DigestAlgorithm;
index: number;
random256!: Uint8Array;

constructor(algo: DigestAlgorithm) {
this.algo = algo;
this.index = 0;
}

static async new({algo, seed}: {algo: string, seed: string}): Promise<Random> {
let da: DigestAlgorithm = "BLAKE2B"; //default
if (algo == "BLAKE2B-224") da = "BLAKE2B-224";
if (algo == "BLAKE2B-256") da = "BLAKE2B-256";
if (algo == "BLAKE2B-384") da = "BLAKE2B-384";
if (algo == "BLAKE2B") da = "BLAKE2B";
if (algo == "BLAKE2S") da = "BLAKE2S";
if (algo == "BLAKE3") da = "BLAKE3";
if (algo == "KECCAK-224") da = "KECCAK-224";
if (algo == "KECCAK-256") da = "KECCAK-256";
if (algo == "KECCAK-384") da = "KECCAK-384";
if (algo == "KECCAK-512") da = "KECCAK-512";
if (algo == "SHA-384") da = "SHA-384";
if (algo == "SHA3-224") da = "SHA3-224";
if (algo == "SHA3-256") da = "SHA3-256";
if (algo == "SHA3-384") da = "SHA3-384";
if (algo == "SHA3-512") da = "SHA3-512";
if (algo == "SHAKE128") da = "SHAKE128";
if (algo == "SHAKE256") da = "SHAKE256";
if (algo == "TIGER") da = "TIGER";
if (algo == "RIPEMD-160") da = "RIPEMD-160";
if (algo == "SHA-224") da = "SHA-224";
if (algo == "SHA-256") da = "SHA-256";
if (algo == "SHA-512") da = "SHA-512";
if (algo == "MD4") da = "MD4";
if (algo == "MD5") da = "MD5";
if (algo == "SHA-1") da = "SHA-1";

const random = new Random(da);

random.random256 = new Uint8Array(
await crypto.subtle.digest(
random.algo, new TextEncoder().encode(seed)
)
);

return random;
}
}
I think I was hoping Deno and/or TypeScript would allow for something like the following so that there is no need for all the "if" statements, which is error prone and tedious. More importantly, whenever Deno adds a new one to DigestAlgorithm (they added "BLAKE2B-128" 2 months ago), I would also need to change the code to add another "if" statement. What I was hoping for is something kind of like this:
if (DigestAlgorithm.includes(algo)) {
da = algo;
}
if (DigestAlgorithm.includes(algo)) {
da = algo;
}
If something along those lines can't be accomplished, I'm seeing this as a wider language issue. DigestAlgorithm is just an example. The same problem would come up elsewhere.
marvinh.
marvinh.9mo ago
You don't need all these if statements if you type algo correctly. The problem is in your new method, because there it's typed as string.
class Random {
- static async new({algo, seed}: {algo: string, seed: string}): Promise<Random> {
+ static async new({algo, seed}: {algo: DigestAlgorithm, seed: string}): Promise<Random> {
}
}
class Random {
- static async new({algo, seed}: {algo: string, seed: string}): Promise<Random> {
+ static async new({algo, seed}: {algo: DigestAlgorithm, seed: string}): Promise<Random> {
}
}
Quantum
Quantum9mo ago
That is true, but then the calling code would need to have the "if" statements.
marvinh.
marvinh.9mo ago
if the value is provided by the user or something you can use a type guard:
function isValidAlgo(str: string): str is DigestAlgorithm {
return DigestAlgorithm.includes(str);
}
function isValidAlgo(str: string): str is DigestAlgorithm {
return DigestAlgorithm.includes(str);
}
That way you can tell TS that this is of type DigestAlgorithm
marvinh.
marvinh.9mo ago
Documentation - Narrowing
Understand how TypeScript uses JavaScript knowledge to reduce the amount of type syntax in your projects.
Quantum
Quantum9mo ago
The "str is DigestAlgorithm" is interesting and might be what allows DigestAlgorithm.includes(algo) to work. How would I implement that on my "new" method above? Something like this doesn't quite work: static async new({algo, seed}: {algo: string, seed: string}): Promise<Random>, algo is DigestAlgorithm {
cknight
cknight9mo ago
Why? await Random.new({algo: "SHA-256", seed: "my seed string"}); would compile fine, but await Random.new({algo: "blah blah blah", seed: "my seed string"}); would not, if the signature is as Marvin described
Quantum
Quantum9mo ago
Maybe the algo comes from the database or maybe it comes from user input or somewhere else. Something needs to check it against DigestAlgorithm somewhere and the best that I could see how to do that until now was with "if" statements or something. What @marvinh. seemed to indicate is there is a way to cast algo to a DigestAlgorithm in the method signature that could then be used with "DigestAlgorithm.includes", which is interesting and might be what is needed here.
marvinh.
marvinh.9mo ago
you should never cast types in the method signature if not necessary. Types are there to make invalid states possible. Passing any random string to your Random class is not valid, so you should really drop the algo: string type because not any string is a valid algorithm. That solves all of your problems. The casting is only necessary if you receive the value from untrusted input like from an http request or something. Those kinds of type guards/assertions should only live at the outer edge of your program or library.
Quantum
Quantum9mo ago
Yes, I understand and agree with casting being necessary when input is coming from untrusted source and should only live at outer edge of program or library. The new method is the outer edge of the library where the input comes in. I am attempting to work in the DigestAlgorithm.includes(algo) that you showed above, but I am getting this error here: 'DigestAlgorithm' only refers to a type, but is being used as a value here.deno-ts(2693) If it is recommended that the new method here only has algo as DigestAlgorithm, I could abide by that too, ok. But in any case, I'm getting that issue when attempting to use includes on DigestAlgorithm.
marvinh.
marvinh.9mo ago
Ah looks like that's only a type in the std lib
Quantum
Quantum9mo ago
One way or another, I am hoping to get away from needing to have all those "if" statements and I am more than happy to adhere to best practices that you are sharing with me.
marvinh.
marvinh.9mo ago
In that case use the type guard. Here is a full example:
import { type DigestAlgorithm } from "https://deno.land/std@0.202.0/crypto/mod.ts";
import { digestAlgorithms } from "https://deno.land/std/crypto/_wasm/mod.ts";

// deno-lint-ignore no-explicit-any
function isValidAlgorithm(str: any): str is DigestAlgorithm {
return digestAlgorithms.includes(str);
}

export default class Random {
algo: DigestAlgorithm;
index: number;
random256!: Uint8Array;

constructor(algo: DigestAlgorithm) {
this.algo = algo;
this.index = 0;
}

static async new({
algo,
seed,
}: {
algo: string;
seed: string;
}): Promise<Random> {
let da: DigestAlgorithm = "BLAKE2B";
if (isValidAlgorithm(algo)) {
da = algo;
}

const random = new Random(da);

random.random256 = new Uint8Array(
await crypto.subtle.digest(random.algo, new TextEncoder().encode(seed))
);

return random;
}
}
import { type DigestAlgorithm } from "https://deno.land/std@0.202.0/crypto/mod.ts";
import { digestAlgorithms } from "https://deno.land/std/crypto/_wasm/mod.ts";

// deno-lint-ignore no-explicit-any
function isValidAlgorithm(str: any): str is DigestAlgorithm {
return digestAlgorithms.includes(str);
}

export default class Random {
algo: DigestAlgorithm;
index: number;
random256!: Uint8Array;

constructor(algo: DigestAlgorithm) {
this.algo = algo;
this.index = 0;
}

static async new({
algo,
seed,
}: {
algo: string;
seed: string;
}): Promise<Random> {
let da: DigestAlgorithm = "BLAKE2B";
if (isValidAlgorithm(algo)) {
da = algo;
}

const random = new Random(da);

random.random256 = new Uint8Array(
await crypto.subtle.digest(random.algo, new TextEncoder().encode(seed))
);

return random;
}
}
Quantum
Quantum9mo ago
Oh nice! I wasn't even looking at the lower cased digestAlgorithms that allows you to call includes on it. Yes, your guard approach is excellent and works nicely. Thanks for this and the insights!