Skip to content

Instantly share code, notes, and snippets.

@tscholl2
Last active August 20, 2025 15:02
Show Gist options
  • Save tscholl2/dc7dc15dc132ea70a98e8542fefffa28 to your computer and use it in GitHub Desktop.
Save tscholl2/dc7dc15dc132ea70a98e8542fefffa28 to your computer and use it in GitHub Desktop.

Revisions

  1. tscholl2 revised this gist Mar 22, 2018. 2 changed files with 0 additions and 7 deletions.
    6 changes: 0 additions & 6 deletions aes.go
    Original file line number Diff line number Diff line change
    @@ -1,9 +1,3 @@
    /*
    To double check aes.js, I wrote the same thing in go.
    Because of standards, we should be able to encrypt in js
    and decrypt in go. Seems to work fine, with some odd padding.
    */

    package main

    import (
    1 change: 0 additions & 1 deletion aes.py
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,3 @@

    import hashlib
    import os
    from binascii import hexlify, unhexlify
  2. Travis Scholl revised this gist Sep 7, 2017. 3 changed files with 9 additions and 39 deletions.
    2 changes: 1 addition & 1 deletion aes.go
    Original file line number Diff line number Diff line change
    @@ -56,5 +56,5 @@ func main() {
    c := encrypt("hello", "world")
    fmt.Println(c)
    fmt.Println(decrypt("hello", c))
    // fmt.Println(decrypt("hello", "2e449ff2896808d3-dd0739b25a4fb37d86c3891d-2d4b30ad60881030956ef1b2547c839d7935fc28eb537455cb6388be776cd664"))
    fmt.Println(decrypt("hello", "c2932347953ad4a4-25f496d260de9c150fc9e4c6-20bc1f8439796cc914eb783b9996a8d9c32d45e2df"))
    }
    41 changes: 7 additions & 34 deletions aes.js
    Original file line number Diff line number Diff line change
    @@ -1,21 +1,19 @@
    const encoder = new TextEncoder("utf-8");
    /**
    * Encodes a utf8 string as a byte array.
    * @param {String} str
    * @returns {Uint8Array}
    */
    function str2buf(str) {
    return encoder.encode(str);
    return new TextEncoder("utf-8").encode(str);
    }

    const decoder = new TextDecoder("utf-8");
    /**
    * Decodes a byte array as a utf8 string.
    * @param {Uint8Array} buffer
    * @returns {String}
    */
    function buf2str(buffer) {
    return decoder.decode(buffer); // TextDecoder RUINS THE BUFFER
    return new TextDecoder("utf-8").decode(buffer);
    }

    /**
    @@ -40,31 +38,6 @@ function buf2hex(buffer) {
    .join("");
    }

    /**
    * Pad the end of the buffer with `0`'s
    * until the length is 0 mod 16.
    * @param {Uint8Array} buffer
    * @returns {Uint8Array}
    */
    function padBuffer(buffer) {
    const paddedBuffer = new Uint8Array(buffer.length + (16 - buffer.length % 16) % 16);
    buffer.forEach((x, i) => (paddedBuffer[i] = x));
    return paddedBuffer;
    }

    /**
    * Strip any `0`'s at the end of a Uint8Array buffer.
    * @param {Uint8Array} buffer
    * @returns {Uint8Array}
    */
    function stripBuffer(buffer) {
    i = buffer.length - 1;
    while (buffer[i] === 0 && i >= 0) {
    i--;
    }
    return buffer.slice(0, i + 1);
    }

    /**
    * Given a passphrase, this generates a crypto key
    * using `PBKDF2` with SHA256 and 1000 iterations.
    @@ -101,7 +74,7 @@ function deriveKey(passphrase, salt) {
    */
    function encrypt(passphrase, plaintext) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const data = padBuffer(str2buf(plaintext));
    const data = str2buf(plaintext);
    return deriveKey(passphrase).then(([key, salt]) =>
    crypto.subtle
    .encrypt({ name: "AES-GCM", iv }, key, data)
    @@ -120,7 +93,7 @@ function decrypt(passphrase, saltIvCipherHex) {
    const [salt, iv, data] = saltIvCipherHex.split("-").map(hex2buf);
    return deriveKey(passphrase, salt)
    .then(([key]) => crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data))
    .then(v => buf2str(stripBuffer(new Uint8Array(v))));
    .then(v => buf2str(new Uint8Array(v)));
    }

    // EXAMPLE
    @@ -132,6 +105,6 @@ encrypt("hello", "world")
    decrypt(
    "hello",
    "2e449ff2896808d3-dd0739b25a4fb37d86c3891d-2d4b30ad60881030956ef1b2547c839d7935fc28eb537455cb6388be776cd664",
    ).then(console.log);
    */
    "6102677198e41d98-84c95e2d7caf6f2d4ccbfe3c-3093cef35d0dba7a24d37f7d4580b5ad83c154329c",
    ).then(console.log);
    */
    5 changes: 1 addition & 4 deletions aes.py
    Original file line number Diff line number Diff line change
    @@ -16,8 +16,6 @@ def encrypt(passphrase: str, plaintext: str) -> str:
    aes = AESGCM(key)
    iv = os.urandom(12)
    plaintext = plaintext.encode("utf8")
    while len(plaintext) % 16 != 0:
    plaintext += bytes([0])
    ciphertext = aes.encrypt(iv, plaintext, None)
    return "%s-%s-%s" % (hexlify(salt).decode("utf8"), hexlify(iv).decode("utf8"), hexlify(ciphertext).decode("utf8"))

    @@ -27,12 +25,11 @@ def decrypt(passphrase: str, ciphertext: str) -> str:
    key, _ = deriveKey(passphrase, salt)
    aes = AESGCM(key)
    plaintext = aes.decrypt(iv, ciphertext, None)
    while plaintext[-1] == 0:
    plaintext = plaintext[:-1]
    return plaintext.decode("utf8")


    if __name__ == "__main__":
    ciphertext = encrypt("hello", "world")
    print(ciphertext)
    print(decrypt("hello", ciphertext))
    print(decrypt("hello", "6102677198e41d98-84c95e2d7caf6f2d4ccbfe3c-3093cef35d0dba7a24d37f7d4580b5ad83c154329c"))
  3. Travis Scholl revised this gist Sep 7, 2017. 3 changed files with 104 additions and 89 deletions.
    47 changes: 21 additions & 26 deletions aes.go
    Original file line number Diff line number Diff line change
    @@ -20,46 +20,41 @@ import (

    func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
    if salt == nil {
    salt = make([]byte, 16)
    salt = make([]byte, 8)
    // http://www.ietf.org/rfc/rfc2898.txt
    // Salt.
    rand.Read(salt)
    }
    return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
    }

    func encrypt(passphrase, plaintext string) string {
    key, salt := deriveKey(passphrase, nil)
    iv := make([]byte, 12)
    // http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
    // Section 8.2
    rand.Read(iv)
    b, _ := aes.NewCipher(key)
    aesgcm, _ := cipher.NewGCM(b)
    data := aesgcm.Seal(nil, iv, []byte(plaintext), nil)
    return hex.EncodeToString(salt) + "-" + hex.EncodeToString(iv) + "-" + hex.EncodeToString(data)
    }

    func decrypt(passphrase, ciphertext string) string {
    arr := strings.Split(ciphertext, "-")
    salt, _ := hex.DecodeString(arr[0])
    iv, _ := hex.DecodeString(arr[1])
    data, _ := hex.DecodeString(arr[2])
    key, _ := deriveKey(passphrase, salt)
    b, _ := aes.NewCipher(key)
    mode := cipher.NewCBCDecrypter(b, iv)
    mode.CryptBlocks(data, data)
    aesgcm, _ := cipher.NewGCM(b)
    data, _ = aesgcm.Open(nil, iv, data, nil)
    return string(data)
    }

    func encrypt(passphrase, plaintext string) string {
    key, salt := deriveKey(passphrase, nil)
    b, _ := aes.NewCipher(key)
    iv := make([]byte, 16)
    rand.Read(iv)
    mode := cipher.NewCBCEncrypter(b, iv)
    data := []byte(plaintext)
    for len(data)%16 != 0 {
    data = append(data, 11) // I don't know why, but it seems that using web crypto you get extra 11's
    }
    mode.CryptBlocks(data, data)
    return hex.EncodeToString(salt) + "-" + hex.EncodeToString(iv) + "-" + hex.EncodeToString(data)
    }

    func main() {
    passphrase := "world"
    ciphertext := "de8d8b4d845c12b3085059d652403735-8e86a6ca52f4c44c744a3a0bee0c2d7d-778f133498a21f1b7aeb7c6f2e82bfd5"
    plaintext := decrypt(passphrase, ciphertext)
    fmt.Println([]byte(plaintext))

    plaintext = "hello"
    ciphertext = encrypt(passphrase, plaintext)
    fmt.Println(ciphertext)
    fmt.Println(decrypt(passphrase, ciphertext))
    c := encrypt("hello", "world")
    fmt.Println(c)
    fmt.Println(decrypt("hello", c))
    // fmt.Println(decrypt("hello", "2e449ff2896808d3-dd0739b25a4fb37d86c3891d-2d4b30ad60881030956ef1b2547c839d7935fc28eb537455cb6388be776cd664"))
    }
    121 changes: 70 additions & 51 deletions aes.js
    Original file line number Diff line number Diff line change
    @@ -2,68 +2,87 @@ const encoder = new TextEncoder("utf-8");
    /**
    * Encodes a utf8 string as a byte array.
    * @param {String} str
    * @returns {Promise<Uint8Array>}
    * @returns {Uint8Array}
    */
    function str2buf(str) {
    return new Promise(resolve => resolve(encoder.encode(str)));
    return encoder.encode(str);
    }

    const decoder = new TextDecoder("utf-8");
    /**
    * Decodes a byte array as a utf8 string.
    * @param {Uint8Array} buffer
    * @returns {Promise<String>}
    * @returns {String}
    */
    function buf2str(buffer) {
    return new Promise(resolve => resolve(decoder.decode(buffer)));
    return decoder.decode(buffer); // TextDecoder RUINS THE BUFFER
    }

    /**
    * Decodes a string of hex to a byte array.
    * @param {String} hexStr
    * @returns {Promise<Uint8Array>}
    * @returns {Uint8Array}
    */
    function hex2buf(hexStr) {
    return new Promise(resolve =>
    resolve(new Uint8Array(hexStr.match(/.{2}/g).map(h => parseInt(h, 16)))),
    );
    return new Uint8Array(hexStr.match(/.{2}/g).map(h => parseInt(h, 16)));
    }

    /**
    * Encodes a byte array as a string of hex.
    * @param {Uint8Array} buffer
    * @returns {Promise<String>}
    * @returns {String}
    */
    function buf2hex(buffer) {
    return new Promise(resolve => {
    const arr = Array.prototype.slice.call(new Uint8Array(buffer));
    resolve(
    arr
    .map(x => [x >> 4, x & 15])
    .map(ab => ab.map(x => x.toString(16)).join(""))
    .join(""),
    );
    });
    return Array.prototype.slice
    .call(new Uint8Array(buffer))
    .map(x => [x >> 4, x & 15])
    .map(ab => ab.map(x => x.toString(16)).join(""))
    .join("");
    }

    /**
    * Pad the end of the buffer with `0`'s
    * until the length is 0 mod 16.
    * @param {Uint8Array} buffer
    * @returns {Uint8Array}
    */
    function padBuffer(buffer) {
    const paddedBuffer = new Uint8Array(buffer.length + (16 - buffer.length % 16) % 16);
    buffer.forEach((x, i) => (paddedBuffer[i] = x));
    return paddedBuffer;
    }

    /**
    * Strip any `0`'s at the end of a Uint8Array buffer.
    * @param {Uint8Array} buffer
    * @returns {Uint8Array}
    */
    function stripBuffer(buffer) {
    i = buffer.length - 1;
    while (buffer[i] === 0 && i >= 0) {
    i--;
    }
    return buffer.slice(0, i + 1);
    }

    /**
    * Given a passphrase, this generates a crypto key
    * using `PBKDF2` with SHA256 and 1000 iterations.
    * If no salt is given, a new one is generated.
    * The return value is an array of `[key, salt]`.
    * @param {String} passPhrase
    * @param {String} passphrase
    * @param {UInt8Array} salt [salt=random bytes]
    * @returns {Promise<[CryptoKey,UInt8Array]>}
    */
    function deriveKey(passPhrase, salt) {
    salt = salt || crypto.getRandomValues(new Uint8Array(16));
    return str2buf(passPhrase)
    .then(buffer => crypto.subtle.importKey("raw", buffer, "PBKDF2", false, ["deriveKey"]))
    function deriveKey(passphrase, salt) {
    salt = salt || crypto.getRandomValues(new Uint8Array(8));
    return crypto.subtle
    .importKey("raw", str2buf(passphrase), "PBKDF2", false, ["deriveKey"])
    .then(key =>
    crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" },
    key,
    { name: "AES-CBC", length: 256 },
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"],
    ),
    @@ -74,45 +93,45 @@ function deriveKey(passPhrase, salt) {
    /**
    * Given a passphrase and some plaintext, this derives a key
    * (generating a new salt), and then encrypts the plaintext with the derived
    * key using AES-CBC. The ciphertext, salt, and iv are hex encoded and joined
    * key using AES-GCM. The ciphertext, salt, and iv are hex encoded and joined
    * by a "-". So the result is `"salt-iv-ciphertext"`.
    * @param {String} passPhrase
    * @param {String} plainText
    * @param {String} passphrase
    * @param {String} plaintext
    * @returns {Promise<String>}
    */
    function encrypt(passPhrase, plainText) {
    const iv = crypto.getRandomValues(new Uint8Array(16));
    return Promise.all([deriveKey(passPhrase), str2buf(plainText)]).then(([[key, salt], data]) =>
    crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, data).then(ciphertext => {
    return Promise.all([buf2hex(salt), buf2hex(iv), buf2hex(ciphertext)]).then(a => a.join("-"));
    }),
    function encrypt(passphrase, plaintext) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const data = padBuffer(str2buf(plaintext));
    return deriveKey(passphrase).then(([key, salt]) =>
    crypto.subtle
    .encrypt({ name: "AES-GCM", iv }, key, data)
    .then(ciphertext => `${buf2hex(salt)}-${buf2hex(iv)}-${buf2hex(ciphertext)}`),
    );
    }

    /**
    * Given a key and ciphertext (in the form of a string) as given by `encrypt`,
    * this decrypts the ciphertext and returns the original plaintext
    * @param {String} passPhrase
    * @param {String} passphrase
    * @param {String} saltIvCipherHex
    * @returns {Promise<String>}
    */
    function decrypt(passPhrase, saltIvCipherHex) {
    const [saltHex, ivHex, cipherHex] = saltIvCipherHex.split("-");
    return Promise.all([
    hex2buf(saltHex).then(salt => deriveKey(passPhrase, salt).then(([key, salt]) => key)),
    hex2buf(ivHex),
    hex2buf(cipherHex),
    ])
    .then(([key, iv, data]) => crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data))
    .then(buf2str);
    function decrypt(passphrase, saltIvCipherHex) {
    const [salt, iv, data] = saltIvCipherHex.split("-").map(hex2buf);
    return deriveKey(passphrase, salt)
    .then(([key]) => crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data))
    .then(v => buf2str(stripBuffer(new Uint8Array(v))));
    }

    /*
    // EXAMPLE
    const s = "hello world";
    const k = "key";
    encrypt(k, s)
    .then(v => console.log("ENCRYPTED", v) || v)
    .then(v => decrypt(k, v))
    .then(v => console.log("DECRYPTED ", v) || v);
    */
    /*
    encrypt("hello", "world")
    .then(v => console.log("ENCRYPTED", v) || v)
    .then(v => decrypt("hello", v))
    .then(v => console.log("DECRYPTED ", v) || v);
    decrypt(
    "hello",
    "2e449ff2896808d3-dd0739b25a4fb37d86c3891d-2d4b30ad60881030956ef1b2547c839d7935fc28eb537455cb6388be776cd664",
    ).then(console.log);
    */
    25 changes: 13 additions & 12 deletions aes.py
    Original file line number Diff line number Diff line change
    @@ -1,37 +1,38 @@

    import hashlib
    import os
    import pyaes # pip install pyaes
    from binascii import hexlify, unhexlify
    from cryptography.hazmat.primitives.ciphers.aead import AESGCM


    def deriveKey(passphrase: str, salt: bytes=None) -> [str, bytes]:
    if salt is None:
    salt = os.urandom(16)
    salt = os.urandom(8)
    return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf8"), salt, 1000), salt


    def encrypt(passphrase: str, plaintext: str) -> str:
    key, salt = deriveKey(passphrase)
    iv = os.urandom(16)
    aes = pyaes.AESModeOfOperationCBC(key, iv=iv)
    aes = AESGCM(key)
    iv = os.urandom(12)
    plaintext = plaintext.encode("utf8")
    while len(plaintext) % 16 != 0:
    plaintext += bytes([0xb]) # TODO this isn't working
    ciphertext = aes.encrypt(plaintext)
    plaintext += bytes([0])
    ciphertext = aes.encrypt(iv, plaintext, None)
    return "%s-%s-%s" % (hexlify(salt).decode("utf8"), hexlify(iv).decode("utf8"), hexlify(ciphertext).decode("utf8"))


    def decrypt(passphrase: str, ciphertext: str) -> str:
    salt, iv, ciphertext = map(unhexlify, ciphertext.split("-"))
    key, _ = deriveKey(passphrase, salt)
    aes = pyaes.AESModeOfOperationCBC(key, iv=iv)
    return aes.decrypt(ciphertext).decode("utf8")
    aes = AESGCM(key)
    plaintext = aes.decrypt(iv, ciphertext, None)
    while plaintext[-1] == 0:
    plaintext = plaintext[:-1]
    return plaintext.decode("utf8")


    if __name__ == "__main__":
    passphrase = "hello"
    plaintext = "world"
    ciphertext = encrypt(passphrase, plaintext)
    ciphertext = encrypt("hello", "world")
    print(ciphertext)
    print(decrypt(passphrase, ciphertext))
    print(decrypt("hello", ciphertext))
  4. tscholl2 revised this gist Sep 6, 2017. 1 changed file with 37 additions and 0 deletions.
    37 changes: 37 additions & 0 deletions aes.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,37 @@

    import hashlib
    import os
    import pyaes # pip install pyaes
    from binascii import hexlify, unhexlify


    def deriveKey(passphrase: str, salt: bytes=None) -> [str, bytes]:
    if salt is None:
    salt = os.urandom(16)
    return hashlib.pbkdf2_hmac("sha256", passphrase.encode("utf8"), salt, 1000), salt


    def encrypt(passphrase: str, plaintext: str) -> str:
    key, salt = deriveKey(passphrase)
    iv = os.urandom(16)
    aes = pyaes.AESModeOfOperationCBC(key, iv=iv)
    plaintext = plaintext.encode("utf8")
    while len(plaintext) % 16 != 0:
    plaintext += bytes([0xb]) # TODO this isn't working
    ciphertext = aes.encrypt(plaintext)
    return "%s-%s-%s" % (hexlify(salt).decode("utf8"), hexlify(iv).decode("utf8"), hexlify(ciphertext).decode("utf8"))


    def decrypt(passphrase: str, ciphertext: str) -> str:
    salt, iv, ciphertext = map(unhexlify, ciphertext.split("-"))
    key, _ = deriveKey(passphrase, salt)
    aes = pyaes.AESModeOfOperationCBC(key, iv=iv)
    return aes.decrypt(ciphertext).decode("utf8")


    if __name__ == "__main__":
    passphrase = "hello"
    plaintext = "world"
    ciphertext = encrypt(passphrase, plaintext)
    print(ciphertext)
    print(decrypt(passphrase, ciphertext))
  5. tscholl2 revised this gist Sep 6, 2017. 1 changed file with 65 additions and 0 deletions.
    65 changes: 65 additions & 0 deletions aes.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,65 @@
    /*
    To double check aes.js, I wrote the same thing in go.
    Because of standards, we should be able to encrypt in js
    and decrypt in go. Seems to work fine, with some odd padding.
    */

    package main

    import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strings"

    "golang.org/x/crypto/pbkdf2"
    )

    func deriveKey(passphrase string, salt []byte) ([]byte, []byte) {
    if salt == nil {
    salt = make([]byte, 16)
    rand.Read(salt)
    }
    return pbkdf2.Key([]byte(passphrase), salt, 1000, 32, sha256.New), salt
    }

    func decrypt(passphrase, ciphertext string) string {
    arr := strings.Split(ciphertext, "-")
    salt, _ := hex.DecodeString(arr[0])
    iv, _ := hex.DecodeString(arr[1])
    data, _ := hex.DecodeString(arr[2])
    key, _ := deriveKey(passphrase, salt)
    b, _ := aes.NewCipher(key)
    mode := cipher.NewCBCDecrypter(b, iv)
    mode.CryptBlocks(data, data)
    return string(data)
    }

    func encrypt(passphrase, plaintext string) string {
    key, salt := deriveKey(passphrase, nil)
    b, _ := aes.NewCipher(key)
    iv := make([]byte, 16)
    rand.Read(iv)
    mode := cipher.NewCBCEncrypter(b, iv)
    data := []byte(plaintext)
    for len(data)%16 != 0 {
    data = append(data, 11) // I don't know why, but it seems that using web crypto you get extra 11's
    }
    mode.CryptBlocks(data, data)
    return hex.EncodeToString(salt) + "-" + hex.EncodeToString(iv) + "-" + hex.EncodeToString(data)
    }

    func main() {
    passphrase := "world"
    ciphertext := "de8d8b4d845c12b3085059d652403735-8e86a6ca52f4c44c744a3a0bee0c2d7d-778f133498a21f1b7aeb7c6f2e82bfd5"
    plaintext := decrypt(passphrase, ciphertext)
    fmt.Println([]byte(plaintext))

    plaintext = "hello"
    ciphertext = encrypt(passphrase, plaintext)
    fmt.Println(ciphertext)
    fmt.Println(decrypt(passphrase, ciphertext))
    }
  6. tscholl2 created this gist Sep 6, 2017.
    118 changes: 118 additions & 0 deletions aes.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,118 @@
    const encoder = new TextEncoder("utf-8");
    /**
    * Encodes a utf8 string as a byte array.
    * @param {String} str
    * @returns {Promise<Uint8Array>}
    */
    function str2buf(str) {
    return new Promise(resolve => resolve(encoder.encode(str)));
    }

    const decoder = new TextDecoder("utf-8");
    /**
    * Decodes a byte array as a utf8 string.
    * @param {Uint8Array} buffer
    * @returns {Promise<String>}
    */
    function buf2str(buffer) {
    return new Promise(resolve => resolve(decoder.decode(buffer)));
    }

    /**
    * Decodes a string of hex to a byte array.
    * @param {String} hexStr
    * @returns {Promise<Uint8Array>}
    */
    function hex2buf(hexStr) {
    return new Promise(resolve =>
    resolve(new Uint8Array(hexStr.match(/.{2}/g).map(h => parseInt(h, 16)))),
    );
    }

    /**
    * Encodes a byte array as a string of hex.
    * @param {Uint8Array} buffer
    * @returns {Promise<String>}
    */
    function buf2hex(buffer) {
    return new Promise(resolve => {
    const arr = Array.prototype.slice.call(new Uint8Array(buffer));
    resolve(
    arr
    .map(x => [x >> 4, x & 15])
    .map(ab => ab.map(x => x.toString(16)).join(""))
    .join(""),
    );
    });
    }

    /**
    * Given a passphrase, this generates a crypto key
    * using `PBKDF2` with SHA256 and 1000 iterations.
    * If no salt is given, a new one is generated.
    * The return value is an array of `[key, salt]`.
    * @param {String} passPhrase
    * @param {UInt8Array} salt [salt=random bytes]
    * @returns {Promise<[CryptoKey,UInt8Array]>}
    */
    function deriveKey(passPhrase, salt) {
    salt = salt || crypto.getRandomValues(new Uint8Array(16));
    return str2buf(passPhrase)
    .then(buffer => crypto.subtle.importKey("raw", buffer, "PBKDF2", false, ["deriveKey"]))
    .then(key =>
    crypto.subtle.deriveKey(
    { name: "PBKDF2", salt, iterations: 1000, hash: "SHA-256" },
    key,
    { name: "AES-CBC", length: 256 },
    false,
    ["encrypt", "decrypt"],
    ),
    )
    .then(key => [key, salt]);
    }

    /**
    * Given a passphrase and some plaintext, this derives a key
    * (generating a new salt), and then encrypts the plaintext with the derived
    * key using AES-CBC. The ciphertext, salt, and iv are hex encoded and joined
    * by a "-". So the result is `"salt-iv-ciphertext"`.
    * @param {String} passPhrase
    * @param {String} plainText
    * @returns {Promise<String>}
    */
    function encrypt(passPhrase, plainText) {
    const iv = crypto.getRandomValues(new Uint8Array(16));
    return Promise.all([deriveKey(passPhrase), str2buf(plainText)]).then(([[key, salt], data]) =>
    crypto.subtle.encrypt({ name: "AES-CBC", iv }, key, data).then(ciphertext => {
    return Promise.all([buf2hex(salt), buf2hex(iv), buf2hex(ciphertext)]).then(a => a.join("-"));
    }),
    );
    }

    /**
    * Given a key and ciphertext (in the form of a string) as given by `encrypt`,
    * this decrypts the ciphertext and returns the original plaintext
    * @param {String} passPhrase
    * @param {String} saltIvCipherHex
    * @returns {Promise<String>}
    */
    function decrypt(passPhrase, saltIvCipherHex) {
    const [saltHex, ivHex, cipherHex] = saltIvCipherHex.split("-");
    return Promise.all([
    hex2buf(saltHex).then(salt => deriveKey(passPhrase, salt).then(([key, salt]) => key)),
    hex2buf(ivHex),
    hex2buf(cipherHex),
    ])
    .then(([key, iv, data]) => crypto.subtle.decrypt({ name: "AES-CBC", iv }, key, data))
    .then(buf2str);
    }

    /*
    // EXAMPLE
    const s = "hello world";
    const k = "key";
    encrypt(k, s)
    .then(v => console.log("ENCRYPTED", v) || v)
    .then(v => decrypt(k, v))
    .then(v => console.log("DECRYPTED ", v) || v);
    */