ekyc

Electronic KYC process with uploading ID document using OAuth 2.1 (experimental)
Log | Files | Refs | README | LICENSE

crypto.ts (4425B)


      1 import { Code } from "#core/domain/code.ts";
      2 import { CODE_DIGITS, TOKEN_BYTES } from "#core/domain/constants.ts";
      3 import { Token } from "#core/domain/token.ts";
      4 import { UUID } from "#core/domain/uuid.ts";
      5 import { concat } from "$std/bytes/concat.ts";
      6 import { timingSafeEqual } from "$std/crypto/timing_safe_equal.ts";
      7 import { decodeBase58, encodeBase58 } from "$std/encoding/base58.ts";
      8 import { encodeBase64Url } from "$std/encoding/base64url.ts";
      9 import sodium from "libsodium-wrappers-sumo";
     10 
     11 await sodium.ready;
     12 
     13 const encoder = new TextEncoder();
     14 const decoder = new TextDecoder();
     15 
     16 export class CryptoFailure extends Error {
     17   constructor(options?: ErrorOptions) {
     18     super("Crypto failure", options);
     19   }
     20 }
     21 
     22 /***************************************************
     23  * Constant time equal (Safe string compare)
     24  */
     25 
     26 export function isSafeEqual(
     27   left: object | string | null | undefined,
     28   right: object | string | null | undefined,
     29 ) {
     30   return left !== null &&
     31     left !== undefined &&
     32     right !== null &&
     33     right !== undefined &&
     34     timingSafeEqual(
     35       encoder.encode(left.toString()),
     36       encoder.encode(right.toString()),
     37     );
     38 }
     39 
     40 /***************************************************
     41  * Crypto-safe random generation
     42  */
     43 
     44 export function nonceUUID() {
     45   return new UUID(crypto.randomUUID());
     46 }
     47 
     48 export function nonceToken(): Token {
     49   return new Token(
     50     encodeBase58(sodium.randombytes_buf(TOKEN_BYTES)),
     51   );
     52 }
     53 
     54 export function nonceCode() {
     55   return new Code(
     56     new Array(CODE_DIGITS)
     57       .fill(null)
     58       .map(() => sodium.randombytes_uniform(10))
     59       .join(""),
     60   );
     61 }
     62 
     63 /***************************************************
     64  * PKCE
     65  */
     66 export async function isCodeVerifier(
     67   challenge: object | string | null,
     68   verifier: string | null,
     69 ): Promise<boolean> {
     70   if (challenge === null && verifier === null) {
     71     return true;
     72   }
     73   const digest = await crypto.subtle.digest(
     74     "SHA-256",
     75     encoder.encode(verifier ?? ""),
     76   );
     77   return isSafeEqual(challenge, encodeBase64Url(digest));
     78 }
     79 
     80 /***************************************************
     81  * Password Hashing
     82  */
     83 
     84 const OPSLIMIT = sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE;
     85 const MEMLIMIT = sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE;
     86 
     87 export class PasswordHash {
     88   static hash(candidate: string) {
     89     try {
     90       return new PasswordHash(
     91         sodium.crypto_pwhash_str(candidate, OPSLIMIT, MEMLIMIT),
     92       );
     93     } catch (cause) {
     94       throw new CryptoFailure({ cause });
     95     }
     96   }
     97 
     98   constructor(private value: string) {
     99   }
    100 
    101   verify(candidate: string) {
    102     try {
    103       if (!sodium.crypto_pwhash_str_verify(this.value, candidate)) {
    104         return false;
    105       }
    106       if (
    107         sodium.crypto_pwhash_str_needs_rehash(this.value, OPSLIMIT, MEMLIMIT)
    108       ) {
    109         this.value = sodium.crypto_pwhash_str(candidate, OPSLIMIT, MEMLIMIT);
    110       }
    111       return true;
    112     } catch (cause) {
    113       throw new CryptoFailure({ cause });
    114     }
    115   }
    116 
    117   toString() {
    118     return this.value;
    119   }
    120 }
    121 
    122 /***************************************************
    123  * Encryption (AEAD)
    124  */
    125 
    126 const AEAD_SECRET_BYTES = sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES;
    127 const AEAD_NONCE_BYTES = sodium.crypto_aead_xchacha20poly1305_IETF_NPUBBYTES;
    128 
    129 export class AEAD {
    130   readonly secret: Uint8Array;
    131 
    132   constructor(secret?: Uint8Array) {
    133     try {
    134       this.secret = secret ?? sodium.randombytes_buf(AEAD_SECRET_BYTES);
    135     } catch (cause) {
    136       throw new CryptoFailure({ cause });
    137     }
    138   }
    139 
    140   encrypt(
    141     message: string,
    142     additional: string | Uint8Array | null = null,
    143   ): string {
    144     try {
    145       const nonce = sodium.randombytes_buf(AEAD_NONCE_BYTES);
    146       return encodeBase58(concat([
    147         nonce,
    148         sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
    149           message,
    150           additional,
    151           nonce,
    152           nonce,
    153           this.secret,
    154         ),
    155       ]));
    156     } catch (cause) {
    157       throw new CryptoFailure({ cause });
    158     }
    159   }
    160 
    161   decrypt(
    162     message: string,
    163     additional: string | Uint8Array | null = null,
    164   ): string {
    165     try {
    166       const bytes = decodeBase58(message);
    167       const nonce = bytes.slice(0, AEAD_NONCE_BYTES);
    168       return decoder.decode(sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
    169         nonce,
    170         bytes.slice(AEAD_NONCE_BYTES),
    171         additional,
    172         nonce,
    173         this.secret,
    174       ));
    175     } catch (cause) {
    176       throw new CryptoFailure({ cause });
    177     }
    178   }
    179 }