Last active
December 20, 2024 04:34
-
-
Save pbnjay/5f31d67f5b4041c1769d6ae9850d56bd to your computer and use it in GitHub Desktop.
Simple, but reasonably secure approach to storing secrets within an OpenAPI document using AES-256 GCM for en/decryption, and PBKDF2 to derive a key from a supplied passphrase.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package secure_strings | |
import ( | |
"crypto/aes" | |
"crypto/cipher" | |
"crypto/rand" | |
"crypto/sha256" | |
"encoding/base64" | |
"fmt" | |
"strings" | |
"golang.org/x/crypto/pbkdf2" | |
) | |
func encrypt(plaintext, key, iv []byte) ([]byte, error) { | |
block, err := aes.NewCipher(key) | |
if err != nil { | |
return nil, err | |
} | |
aesgcm, err := cipher.NewGCM(block) | |
if err != nil { | |
return nil, err | |
} | |
ciphertext := aesgcm.Seal(nil, iv, plaintext, nil) | |
return ciphertext, nil | |
} | |
func decrypt(ciphertext, key, iv []byte) ([]byte, error) { | |
block, err := aes.NewCipher(key) | |
if err != nil { | |
return nil, err | |
} | |
aesgcm, err := cipher.NewGCM(block) | |
if err != nil { | |
return nil, err | |
} | |
plaintext, err := aesgcm.Open(nil, iv, ciphertext, nil) | |
if err != nil { | |
return nil, err | |
} | |
return plaintext, nil | |
} | |
func IsEncrypted(val string) bool { | |
return strings.HasPrefix(val, "$ag2$v=6a$iv=") | |
} | |
func DecryptValue(passphrase, encrypted string) (string, error) { | |
parts := strings.Split(encrypted, "$") | |
if len(parts) != 6 || parts[0] != "" || parts[1] != "ag2" || parts[2] != "v=6a" || !strings.HasPrefix(parts[3], "iv=") { | |
return "", fmt.Errorf("invalid value") | |
} | |
iv, err := base64.StdEncoding.DecodeString(parts[3][3:]) | |
if err != nil { | |
return "", fmt.Errorf("invalid iv") | |
} | |
salt, err := base64.StdEncoding.DecodeString(parts[4]) | |
if err != nil { | |
return "", fmt.Errorf("invalid salt") | |
} | |
ciphertext, err := base64.StdEncoding.DecodeString(parts[5]) | |
if err != nil { | |
return "", fmt.Errorf("invalid ciphertext") | |
} | |
key := pbkdf2.Key([]byte(passphrase), salt, 600000, 32, sha256.New) | |
result, err := decrypt(ciphertext, key, iv) | |
if err != nil { | |
return "", fmt.Errorf("decryption failed: %e", err) | |
} | |
return string(result), nil | |
} | |
func EncryptValue(passphrase, plaintext string) (string, error) { | |
salt := make([]byte, 18) | |
_, err := rand.Read(salt) | |
if err != nil { | |
return "", err | |
} | |
iv := make([]byte, 12) | |
_, err = rand.Read(iv) | |
if err != nil { | |
return "", err | |
} | |
key := pbkdf2.Key([]byte(passphrase), salt, 600000, 32, sha256.New) | |
result, err := encrypt([]byte(plaintext), key, iv) | |
if err != nil { | |
return "", err | |
} | |
return ("$ag2$v=6a$iv=" + | |
base64.StdEncoding.EncodeToString(iv) + | |
"$" + | |
base64.StdEncoding.EncodeToString(salt) + | |
"$" + | |
base64.StdEncoding.EncodeToString(result)), nil | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# pip3 install pycryptodome | |
import base64 | |
import os | |
from Crypto.Cipher import AES | |
from Crypto.Hash import SHA256 | |
from Crypto.Protocol.KDF import PBKDF2 | |
def encrypt(plaintext, key, iv): | |
cipher = AES.new(key, AES.MODE_GCM, nonce=iv) | |
ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode('utf-8')) | |
return ciphertext + tag | |
def decrypt(ciphertext, key, iv): | |
cipher = AES.new(key, AES.MODE_GCM, nonce=iv) | |
actual_ciphertext, tag = ciphertext[:-16], ciphertext[-16:] | |
plaintext = cipher.decrypt_and_verify(actual_ciphertext, tag).decode('utf-8') | |
return plaintext | |
def is_encrypted(val): | |
return val.startswith("$ag2$v=6a$iv=") | |
def decrypt_value(passphrase, encrypted): | |
parts = encrypted.split("$") | |
if len(parts) != 6 or parts[0] != "" or parts[1] != "ag2" or parts[2] != "v=6a" or not parts[3].startswith("iv="): | |
raise ValueError("Invalid value") | |
iv = base64.b64decode(parts[3][3:]) | |
salt = base64.b64decode(parts[4]) | |
ciphertext = base64.b64decode(parts[5]) | |
key = PBKDF2(passphrase.encode('utf-8'), salt, dkLen=32, count=600000, hmac_hash_module=SHA256) | |
try: | |
result = decrypt(ciphertext, key, iv) | |
except Exception as e: | |
raise ValueError(f"Decryption failed: {e}") | |
return result | |
def encrypt_value(passphrase, plaintext): | |
salt = os.urandom(18) | |
iv = os.urandom(12) | |
key = PBKDF2(passphrase.encode('utf-8'), salt, dkLen=32, count=600000, hmac_hash_module=SHA256) | |
ciphertext = encrypt(plaintext, key, iv) | |
return ( | |
"$ag2$v=6a$iv=" + | |
base64.b64encode(iv).decode('utf-8') + "$" + | |
base64.b64encode(salt).decode('utf-8') + "$" + | |
base64.b64encode(ciphertext).decode('utf-8') | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import CryptoJS from "crypto-js" | |
/** Returns a crypto-JS compatible Word Array, based on the byte array provided */ | |
const toWordArray = (bytes: Uint8Array) => { | |
const words: number[] = [] | |
for (let j = 0; j < bytes.length; j++) { | |
words[j >>> 2] |= bytes[j] << (24 - 8 * (j % 4)) | |
} | |
return CryptoJS.lib.WordArray.create(words, bytes.length) | |
} | |
/** Returns a byte array, based on the crypto-JS compatible word array provided */ | |
const fromWordArray = (wordArray: CryptoJS.lib.WordArray): Uint8Array => { | |
const bytes = new Uint8Array(wordArray.sigBytes) | |
for (let j = 0; j < wordArray.sigBytes; j++) { | |
bytes[j] = (wordArray.words[j >>> 2] >>> (24 - 8 * (j % 4))) & 0xff | |
} | |
return bytes | |
} | |
export const md5 = (s: string): string => { | |
return CryptoJS.MD5(s).toString() | |
} | |
export const base64encode = (s: string | ArrayBuffer): string => { | |
if (typeof s === "string") { | |
return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(s)) | |
} else { | |
return CryptoJS.enc.Base64.stringify(toWordArray(new Uint8Array(s))) | |
} | |
} | |
export const base64decodeString = (s: string): string => { | |
return CryptoJS.enc.Utf8.stringify(CryptoJS.enc.Base64.parse(s)) | |
} | |
export const base64decodeArray = (s: string): Uint8Array => { | |
const res = CryptoJS.enc.Base64.parse(s) | |
return fromWordArray(res) | |
} | |
//////////////////////////////////// | |
const getKeyFromPassphrase = async (passphrase: string, salt: Uint8Array) => { | |
const keyMaterial = await crypto.subtle.importKey( | |
"raw", | |
new TextEncoder().encode(passphrase), | |
"PBKDF2", | |
false, | |
["deriveBits", "deriveKey"] | |
) | |
const key = await crypto.subtle.deriveKey( | |
{ | |
name: "PBKDF2", | |
salt, | |
iterations: 600000, | |
hash: "SHA-256", | |
}, | |
keyMaterial, | |
{ name: "AES-GCM", length: 256 }, | |
true, | |
["encrypt", "decrypt"] | |
) | |
return key | |
} | |
export const encryptString = async ( | |
value: string, | |
passphrase: string | |
): Promise<string> => { | |
const salt = crypto.getRandomValues(new Uint8Array(18)) | |
const key = await getKeyFromPassphrase(passphrase, salt) | |
const iv = crypto.getRandomValues(new Uint8Array(12)) | |
const ciphertext = await crypto.subtle.encrypt( | |
{ | |
name: "AES-GCM", | |
iv: iv, | |
tagLength: 128, | |
}, | |
key, | |
new TextEncoder().encode(value) | |
) | |
// Trying to at least look like a PHC crypt format | |
// ag2 = AES-GCM 256bit | |
// v=6a -- this combo of passphrase => PBKDF2 key derivation | |
// iv = AES init vector | |
return ( | |
"$ag2$v=6a$iv=" + | |
base64encode(iv.buffer) + | |
"$" + | |
base64encode(salt.buffer) + | |
"$" + | |
base64encode(ciphertext) | |
) | |
} | |
export const decryptString = async ( | |
value: string, | |
passphrase: string | |
): Promise<string> => { | |
if (!value.startsWith("$ag2$v=6a$iv=")) { | |
throw "unknown encryption prefix" | |
} | |
const parts = value.split("$") | |
const iv = base64decodeArray(parts[3].slice(3)) | |
const salt = base64decodeArray(parts[4]) | |
const ciphertext = base64decodeArray(parts[5]) | |
const key = await getKeyFromPassphrase(passphrase, salt) | |
const plaintext = await crypto.subtle.decrypt( | |
{ | |
name: "AES-GCM", | |
iv: iv, | |
tagLength: 128, | |
}, | |
key, | |
ciphertext.buffer | |
) | |
return new TextDecoder().decode(plaintext) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment