~bradclawsie/easysecurity

769e2cbeab12e3fbb104c0d4766f4ee1a64a51d8 — Brad Clawsie 9 months ago f0d746e master 0.2.0
change cipher; remove Crypter class; new encrypt/decrypt apis
3 files changed, 66 insertions(+), 102 deletions(-)

M README.md
M mod.ts
M test.ts
M README.md => README.md +15 -31
@@ 7,11 7,17 @@ Basic security API for Typescript that optimizes for easy use

## Important Compatibility Note

Note that there will be breaking changes introduced after version 0.1.9.
These changes will start a 0.2.x version scheme. If you have a dependency
on this library as it is presently developed, I recommend fixing your
use at version 0.1.9 until you can undertake a migration forward. 
Alternately, you may wish to simply fork version 0.1.9.
The 0.2.0 release breaks compatibility with the previous relesae, 0.1.9. This
was noted in the 0.1.9 README. The changes include:

- Using AES-GCM 256 as the default cipher instead of AES-CBC 128.
- Moving `encryptToHex` and `decryptToHex` to static methods that feature
  per-encrypt IVs automatically generated and prepended to the encrypted
  message.
- Removing the `Crypter` class.

No deprecation notices are included as the change of the cipher implies the need
to regenerate encrypted messages generated with versions prior.

## Motivation



@@ 22,7 28,7 @@ In almost all cases, I want to work with values that can be printed and stored
directly, which for me means using hex encoding as a serialization format.

This module is not for cryptography experts. This module is not for people who
want to tune parameters. This module is for people for whom AES-CBC is a "good
want to tune parameters. This module is for people for whom AES-GCM is a "good
enough" cipher, for whom v4 UUIDs are a "good enough" random value, and for whom
sha256 is a "good enough" hash.



@@ 49,31 55,9 @@ randomUUID(); // re-export crypto.randomUUID();
### Encryption/Decryption

```ts
// generate a Crypter using new, random key and IV
// (you can access them via crypter.Key, crypter.IV)
const crypter = Crypter.generate();

// or maybe you already have hex-exported Key and IV that you had stored
const crypter = Crypter.fromHex(hexKey, hexIV);

// or maybe you want to generate new Key and IV
const key = await Key.generate();
const iv = IV.fromString("user@example.com");
const crypter = await new Crypter(key, iv);

// or you want to create Key and IV instances directly from
// hex - exports;
const key = await Key.fromHex(hexKey);
const iv = IV.fromHex(hexIV);

// and you can make these exports easily:
const hexKey = key.toHex();
const hexIV = iv.toHex();

// to encrypt some clear text:
const clearText = "hello world";
const hexCrypted = await crypter.encryptToHex(clearText);

// ...and get the clear text back
const decrypted = await crypter.decryptFromHex(hexCrypted);
const hexCryptedWithIV = await encryptToHex(clearText, key);
const decrypted = await decryptFromHex(hexCryptedWithIV, key);
// clearText == decrypted
```

M mod.ts => mod.ts +33 -66
@@ 35,13 35,13 @@ const isUUID = (s: string): boolean => {
 */
const randomUUID = (): string => crypto.randomUUID();

const AES_CBC = "AES-CBC";
const AES_GCM = "AES-GCM";

/**
 * @class Key provides a simple wrapper for AES CryptoKeys allowing for serialization to hex.
 */
class Key {
  static readonly Params: AesKeyGenParams = { name: AES_CBC, length: 128 };
  static readonly Params: AesKeyGenParams = { name: AES_GCM, length: 256 };
  static readonly Extractable = true;
  static readonly Usages: KeyUsage[] = ["encrypt", "decrypt"];



@@ 154,73 154,40 @@ class IV {
}

/**
 * @class Crypter provides a simple wrapper for encrypting to and decrypting from hex-encoded values
 * encrypt a clear text and hex-encode the resulting encrypted value
 * @param {string} clearText - the string to encrypt
 * @param {Key} key - Key instance
 * @returns {Promise<string>} - the hex-encoding of the encrypted iv + clearText
 */
class Crypter {
  readonly key: Key;
  readonly iv: IV;

  /**
   * @constructor
   * @param {Key} key - the encryption Key
   * @param {IV} iv - the encryption IV
   */
  constructor(key: Key, iv: IV) {
    this.key = key;
    this.iv = iv;
  }

  /**
   * construct a new Crypter using a hex-encoded Key and IV
   * @param {string} hexKey - the hex-encoded encryption Key (from Key.toHex())
   * @param {string} hexIV - the hex-encoded encryption IV (from IV.toHex())
   * @returns {Promise<Crypter>} - a new Crypter constructed from hexKey and hexIV
   */
  static async fromHex(hexKey: string, hexIV: string): Promise<Crypter> {
    return new Crypter(await Key.fromHex(hexKey), IV.fromHex(hexIV));
  }

  /**
   * construct a new Crypter using newly generated Key and IV
   * @returns {Promise<Crypter>} - a new Crypter constructed with generated Key and IV
   */
  static async generate(): Promise<Crypter> {
    return new Crypter(await Key.generate(), IV.generate());
  }

  /**
   * decrypt a hex-encoded encryption output and return the original clear text
   * @param {string} hexEncrypted - the output of a previous call to encryptToHex
   * @returns {Promise<string>} - the decrypted clear text
   */
  async decryptFromHex(hexEncrypted: string): Promise<string> {
    return bytesToString(
      new Uint8Array(
        await crypto.subtle.decrypt(
          { name: AES_CBC, iv: this.iv.bytes },
          this.key.cryptoKey,
          hexToBytes(hexEncrypted),
        ),
async function encryptToHex(clearText: string, key: Key): Promise<string> {
  const iv = IV.generate();
  return iv.toHex() + bytesToHex(
    new Uint8Array(
      await crypto.subtle.encrypt(
        { name: AES_GCM, iv: iv.bytes },
        key.cryptoKey,
        stringToBytes(clearText),
      ),
    );
  }
    ),
  );
}

  /**
   * encrypt a clear text and hex-encode the resulting encrypted value
   * @param {string} clearText - the string to encrypt
   * @returns {Promise<string>} - the decrypted clear text
   */
  async encryptToHex(clearText: string): Promise<string> {
    return bytesToHex(
      new Uint8Array(
        await crypto.subtle.encrypt(
          { name: AES_CBC, iv: this.iv.bytes },
          this.key.cryptoKey,
          stringToBytes(clearText),
        ),
/**
 * @param {string} encrypted - the output of encryptToHex
 * @param {Key} key - Key instance
 * @returns {Promise<string>} - the decrypted clear text
 */
async function decryptFromHex(encrypted: string, key: Key): Promise<string> {
  const iv = IV.fromHex(encrypted.substring(0, 2 * IV.Length));
  return bytesToString(
    new Uint8Array(
      await crypto.subtle.decrypt(
        { name: AES_GCM, iv: iv.bytes },
        key.cryptoKey,
        hexToBytes(encrypted.substring(2 * IV.Length)),
      ),
    );
  }
    ),
  );
}

export { Crypter, isUUID, IV, Key, randomUUID, sha256Hex };
export { decryptFromHex, encryptToHex, isUUID, IV, Key, randomUUID, sha256Hex };

M test.ts => test.ts +18 -5
@@ 1,8 1,17 @@
import {
  assert,
  assertEquals,
  assertNotEquals,
} from "https://deno.land/std@0.208.0/assert/mod.ts";
import { Crypter, isUUID, IV, Key, randomUUID, sha256Hex } from "./mod.ts";
import {
  decryptFromHex,
  encryptToHex,
  isUUID,
  IV,
  Key,
  randomUUID,
  sha256Hex,
} from "./mod.ts";

/**
 * uuid round trip


@@ 53,10 62,14 @@ Deno.test("iv", async () => {
/**
 * encrypt/decrypt
 */
Deno.test("encrypt", async () => {
  const crypter = await Crypter.generate();
Deno.test("encrypt/decrypt", async () => {
  const k = await Key.generate();
  const clearText = "hello world";
  const hexCrypted = await crypter.encryptToHex(clearText);
  const decrypted = await crypter.decryptFromHex(hexCrypted);
  const hexCryptedWithIV = await encryptToHex(clearText, k);
  const decrypted = await decryptFromHex(hexCryptedWithIV, k);
  assertEquals(decrypted, clearText, "round trip encrypt decrypt");
  const hexCryptedWithIV2 = await encryptToHex(clearText, k);
  assertNotEquals(hexCryptedWithIV, hexCryptedWithIV2, "different iv");
  const decrypted2 = await decryptFromHex(hexCryptedWithIV, k);
  assertEquals(decrypted2, clearText, "round trip encrypt decrypt");
});