-
-
Save tscholl2/dc7dc15dc132ea70a98e8542fefffa28 to your computer and use it in GitHub Desktop.
| /* | |
| 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)) | |
| } |
| 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); | |
| */ |
| 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)) |
Sorry I didn't see this comment. You're welcome to use any of this for free. It's not really anything besides how to use the standard libraries.
No problem. Thank you for the answer.
Many thanks for this useful gist
Could I decrypt this password?
:pbkdf2:sha256:10000:128:tMG5uKJ5egwoZu8SEy9xlw==:U+5GmBqgfuD/GjUe7zy/n2MAdU64E6oO9SUT7qjsG1mw2ezSiW2WH2lPgxPKZA7ZHXs3+4Birh01GKRG+V8DeEXlo8DlcgWcIx+NaeWWTMN8u3nzPdgah548UWksu8SIH9iGmjnA3UKCfs8qFCQ7OPGA+rZejrfoTUAJywKTaHM=
I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?
I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?
Good question. I believe the JS implementation is using HMAC in the PRF. I have two reasons to believe this:
- The
prfin thepbkdf2-paramsobject is a MAC generation function https://www.w3.org/TR/WebCryptoAPI/#pbkdf2-params - All the implementations should be able to encrypt/decrypt the same data. If the python and js versions used a different key derivation method, we should not expect that to work. To test this yourself, you can run the code samples and see. If they don't both decrypt the same string to the same value using the same password, that would be a problem!
I notice the python implementation of AESGCM uses the hmac modifier rather than just pbkdf2. Would this not lead to problems with the other provided implementations? Specifically the js implementation that utilizes crypto-subtle's PBKDF2 key derivation but does not include the HMAC modifier?
Good question. I believe the JS implementation is using HMAC in the PRF. I have two reasons to believe this:
- The
prfin thepbkdf2-paramsobject is a MAC generation function https://www.w3.org/TR/WebCryptoAPI/#pbkdf2-params- All the implementations should be able to encrypt/decrypt the same data. If the python and js versions used a different key derivation method, we should not expect that to work. To test this yourself, you can run the code samples and see. If they don't both decrypt the same string to the same value using the same password, that would be a problem!
You're right. Tested and confirmed. Thanks!
I don't see a license here. Is this freely usable? Thanks.