Skip to content

Instantly share code, notes, and snippets.

@pa-0
Forked from anonymous1184/Crypt.ahk
Last active May 21, 2025 23:42
Show Gist options
  • Save pa-0/0ed497c0d4cc12e0fa0b24eb6a70b12d to your computer and use it in GitHub Desktop.
Save pa-0/0ed497c0d4cc12e0fa0b24eb6a70b12d to your computer and use it in GitHub Desktop.
MasterPassword.ahk

Information:

https://redd.it/1051mkc

See the example.ahk in this gist.

Crypt.ahk was modified to make it #Warn-compatible (Crypt.ahk.patch file provided for diff)


Creates a new encrypted password in the defined path:

MasterPassword_Create(Password, Path)

Returns the password, asks for the decryption key if necessary:

MasterPassword(Path)

Clears the password from memory:

MasterPassword("")

To automatically clear the password:

Options := {}
; Options.Inactive := int  ; In minutes
; Options.Lid      := bool ; On lid close
; Options.Lock     := bool ; On lock screen
; Options.Sleep    := bool ; On system sleep
MasterPassword_Clear(Options)

Stop and remove auto-clearing

MasterPassword_Clear("")
; ===============================================================================================================================
; AutoHotkey wrapper for Cryptography API: Next Generation
;
; Author ....: jNizM
; Released ..: 2016-09-15
; Modified ..: 2021-01-04
; Github ....: https://github.com/jNizM/AHK_CNG
; Forum .....: https://www.autohotkey.com/boards/viewtopic.php?f=6&t=23413
; ===============================================================================================================================
class Crypt
{
; ===== PUBLIC CLASS / METHODS ==============================================================================================
class Encrypt
{
String(AlgId, Mode := "", String := "", Key := "", IV := "", Encoding := "utf-8", Output := "BASE64")
{
try
{
; verify the encryption algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.EncryptionAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle.
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; verify the chaining mode
if (CHAINING_MODE := Crypt.Verify.ChainingMode(Mode))
; set chaining mode property.
if !(Crypt.BCrypt.SetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_CHAINING_MODE, CHAINING_MODE))
throw Exception("SetProperty failed", -1)
; generate the key from supplied input key bytes.
if !(KEY_HANDLE := Crypt.BCrypt.GenerateSymmetricKey(ALG_HANDLE, Key, Encoding))
throw Exception("GenerateSymmetricKey failed", -1)
; calculate the block length for the IV.
if !(BLOCK_LENGTH := Crypt.BCrypt.GetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_BLOCK_LENGTH, 4))
throw Exception("GetProperty failed", -1)
; use the key to encrypt the plaintext buffer. for block sized messages, block padding will add an extra block.
cbInput := Crypt.Helper.StrPutVar(String, pbInput, Encoding)
if !(CIPHER_LENGTH := Crypt.BCrypt.Encrypt(KEY_HANDLE, pbInput, cbInput, IV, BLOCK_LENGTH, CIPHER_DATA, Crypt.Constants.BCRYPT_BLOCK_PADDING))
throw Exception("Encrypt failed", -1)
; convert binary data to string (base64 / hex / hexraw)
if !(ENCRYPT := Crypt.Helper.CryptBinaryToString(CIPHER_DATA, CIPHER_LENGTH, Output))
throw Exception("CryptBinaryToString failed", -1)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (KEY_HANDLE)
Crypt.BCrypt.DestroyKey(KEY_HANDLE)
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return ENCRYPT
}
}
class Decrypt
{
String(AlgId, Mode := "", String := "", Key := "", IV := "", Encoding := "utf-8", Input := "BASE64")
{
try
{
; verify the encryption algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.EncryptionAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle.
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; verify the chaining mode
if (CHAINING_MODE := Crypt.Verify.ChainingMode(Mode))
; set chaining mode property.
if !(Crypt.BCrypt.SetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_CHAINING_MODE, CHAINING_MODE))
throw Exception("SetProperty failed", -1)
; generate the key from supplied input key bytes.
if !(KEY_HANDLE := Crypt.BCrypt.GenerateSymmetricKey(ALG_HANDLE, Key, Encoding))
throw Exception("GenerateSymmetricKey failed", -1)
; convert encrypted string (base64 / hex / hexraw) to binary data
if !(CIPHER_LENGTH := Crypt.Helper.CryptStringToBinary(String, CIPHER_DATA, Input))
throw Exception("CryptStringToBinary failed", -1)
; calculate the block length for the IV.
if !(BLOCK_LENGTH := Crypt.BCrypt.GetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_BLOCK_LENGTH, 4))
throw Exception("GetProperty failed", -1)
; use the key to decrypt the data to plaintext buffer
if !(DECRYPT_LENGTH := Crypt.BCrypt.Decrypt(KEY_HANDLE, CIPHER_DATA, CIPHER_LENGTH, IV, BLOCK_LENGTH, DECRYPT_DATA, Crypt.Constants.BCRYPT_BLOCK_PADDING))
throw Exception("Decrypt failed", -1)
; receive the decrypted plaintext
DECRYPT := StrGet(&DECRYPT_DATA, DECRYPT_LENGTH, Encoding)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (KEY_HANDLE)
Crypt.BCrypt.DestroyKey(KEY_HANDLE)
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return DECRYPT
}
}
class Hash
{
String(AlgId, String, Encoding := "utf-8", Output := "HEXRAW")
{
try
{
; verify the hash algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.HashAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; create a hash
if !(HASH_HANDLE := Crypt.BCrypt.CreateHash(ALG_HANDLE))
throw Exception("CreateHash failed", -1)
; hash some data
cbInput := Crypt.Helper.StrPutVar(String, pbInput, Encoding)
if !(Crypt.BCrypt.HashData(HASH_HANDLE, pbInput, cbInput))
throw Exception("HashData failed", -1)
; calculate the length of the hash
if !(HASH_LENGTH := Crypt.BCrypt.GetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_HASH_LENGTH, 4))
throw Exception("GetProperty failed", -1)
; close the hash
if !(Crypt.BCrypt.FinishHash(HASH_HANDLE, HASH_DATA, HASH_LENGTH))
throw Exception("FinishHash failed", -1)
; convert bin to string (base64 / hex)
if !(HASH := Crypt.Helper.CryptBinaryToString(HASH_DATA, HASH_LENGTH, Output))
throw Exception("CryptBinaryToString failed", -1)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (HASH_HANDLE)
Crypt.BCrypt.DestroyHash(HASH_HANDLE)
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return HASH
}
File(AlgId, FileName, Bytes := 1048576, Offset := 0, Length := -1, Encoding := "utf-8", Output := "HEXRAW")
{
try
{
; verify the hash algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.HashAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; create a hash
if !(HASH_HANDLE := Crypt.BCrypt.CreateHash(ALG_HANDLE))
throw Exception("CreateHash failed", -1)
; hash some data
if !(IsObject(File := FileOpen(FileName, "r", Encoding)))
throw Exception("Failed to open file: " FileName, -1)
Length := Length < 0 ? File.Length - Offset : Length
if ((Offset + Length) > File.Length)
throw Exception("Invalid parameters offset / length!", -1)
while (Length > Bytes) && (Dataread := File.RawRead(Data, Bytes))
{
if !(Crypt.BCrypt.HashData(HASH_HANDLE, Data, Dataread))
throw Exception("HashData failed", -1)
Length -= Dataread
}
if (Length > 0)
{
if (Dataread := File.RawRead(Data, Length))
{
if !(Crypt.BCrypt.HashData(HASH_HANDLE, Data, Dataread))
throw Exception("HashData failed", -1)
}
}
; calculate the length of the hash
if !(HASH_LENGTH := Crypt.BCrypt.GetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_HASH_LENGTH, 4))
throw Exception("GetProperty failed", -1)
; close the hash
if !(Crypt.BCrypt.FinishHash(HASH_HANDLE, HASH_DATA, HASH_LENGTH))
throw Exception("FinishHash failed", -1)
; convert bin to string (base64 / hex)
if !(HASH := Crypt.Helper.CryptBinaryToString(HASH_DATA, HASH_LENGTH, Output))
throw Exception("CryptBinaryToString failed", -1)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (File)
File.Close()
if (HASH_HANDLE)
Crypt.BCrypt.DestroyHash(HASH_HANDLE)
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return HASH
}
HMAC(AlgId, String, Hmac, Encoding := "utf-8", Output := "HEXRAW")
{
try
{
; verify the hash algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.HashAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER, Crypt.Constants.BCRYPT_ALG_HANDLE_HMAC_FLAG))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; create a hash
if !(HASH_HANDLE := Crypt.BCrypt.CreateHash(ALG_HANDLE, Hmac, Encoding))
throw Exception("CreateHash failed", -1)
; hash some data
cbInput := Crypt.helper.StrPutVar(String, pbInput, Encoding)
if !(Crypt.BCrypt.HashData(HASH_HANDLE, pbInput, cbInput))
throw Exception("HashData failed", -1)
; calculate the length of the hash
if !(HASH_LENGTH := Crypt.BCrypt.GetProperty(ALG_HANDLE, Crypt.Constants.BCRYPT_HASH_LENGTH, 4))
throw Exception("GetProperty failed", -1)
; close the hash
if !(Crypt.BCrypt.FinishHash(HASH_HANDLE, HASH_DATA, HASH_LENGTH))
throw Exception("FinishHash failed", -1)
; convert bin to string (base64 / hex)
if !(HMAC := Crypt.Helper.CryptBinaryToString(HASH_DATA, HASH_LENGTH, Output))
throw Exception("CryptBinaryToString failed", -1)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (HASH_HANDLE)
Crypt.BCrypt.DestroyHash(HASH_HANDLE)
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return HMAC
}
PBKDF2(AlgId, Password, Salt, Iterations := 4096, KeySize := 256, Encoding := "utf-8", Output := "HEXRAW")
{
try
{
; verify the hash algorithm
if !(ALGORITHM_IDENTIFIER := Crypt.Verify.HashAlgorithm(AlgId))
throw Exception("Wrong ALGORITHM_IDENTIFIER", -1)
; open an algorithm handle
if !(ALG_HANDLE := Crypt.BCrypt.OpenAlgorithmProvider(ALGORITHM_IDENTIFIER, Crypt.Constants.BCRYPT_ALG_HANDLE_HMAC_FLAG))
throw Exception("BCryptOpenAlgorithmProvider failed", -1)
; derives a key from a hash value
if !(Crypt.BCrypt.DeriveKeyPBKDF2(ALG_HANDLE, Password, Salt, Iterations, PBKDF2_DATA, KeySize / 8, Encoding))
throw Exception("CreateHash failed", -1)
; convert bin to string (base64 / hex)
if !(PBKDF2 := Crypt.Helper.CryptBinaryToString(PBKDF2_DATA , KeySize / 8, Output))
throw Exception("CryptBinaryToString failed", -1)
}
catch Exception
{
; represents errors that occur during application execution
throw Exception
}
finally
{
; cleaning up resources
if (ALG_HANDLE)
Crypt.BCrypt.CloseAlgorithmProvider(ALG_HANDLE)
}
return PBKDF2
}
}
; ===== PRIVATE CLASS / METHODS =============================================================================================
/*
CNG BCrypt Functions
https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/
*/
class BCrypt
{
static hBCRYPT := DllCall("LoadLibrary", "str", "bcrypt.dll", "ptr")
static STATUS_SUCCESS := 0
CloseAlgorithmProvider(hAlgorithm)
{
DllCall("bcrypt\BCryptCloseAlgorithmProvider", "ptr", hAlgorithm, "uint", 0)
}
CreateHash(hAlgorithm, hmac := 0, encoding := "utf-8")
{
phHash := pbSecret := cbSecret := 0
if (hmac)
cbSecret := Crypt.helper.StrPutVar(hmac, pbSecret, encoding)
NT_STATUS := DllCall("bcrypt\BCryptCreateHash", "ptr", hAlgorithm
, "ptr*", phHash
, "ptr", pbHashObject := 0
, "uint", cbHashObject := 0
, "ptr", (pbSecret ? &pbSecret : 0)
, "uint", (cbSecret ? cbSecret : 0)
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return phHash
return false
}
DeriveKeyPBKDF2(hPrf, Password, Salt, cIterations, ByRef pbDerivedKey, cbDerivedKey, Encoding := "utf-8")
{
cbPassword := Crypt.Helper.StrPutVar(Password, pbPassword, Encoding)
cbSalt := Crypt.Helper.StrPutVar(Salt, pbSalt, Encoding)
VarSetCapacity(pbDerivedKey, cbDerivedKey, 0)
NT_STATUS := DllCall("bcrypt\BCryptDeriveKeyPBKDF2", "ptr", hPrf
, "ptr", &pbPassword
, "uint", cbPassword
, "ptr", &pbSalt
, "uint", cbSalt
, "int64", cIterations
, "ptr", &pbDerivedKey
, "uint", cbDerivedKey
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return true
return false
}
DestroyHash(hHash)
{
DllCall("bcrypt\BCryptDestroyHash", "ptr", hHash)
}
DestroyKey(hKey)
{
DllCall("bcrypt\BCryptDestroyKey", "ptr", hKey)
}
Decrypt(hKey, ByRef String, cbInput, IV, BCRYPT_BLOCK_LENGTH, ByRef pbOutput, dwFlags)
{
cbOutput := pbIV := cbIV := 0, Encoding := "UTF-8"
VarSetCapacity(pbInput, cbInput, 0)
DllCall("msvcrt\memcpy", "ptr", &pbInput, "ptr", &String, "ptr", cbInput)
if (IV != "")
{
cbIV := VarSetCapacity(pbIV, BCRYPT_BLOCK_LENGTH, 0)
StrPut(IV, &pbIV, BCRYPT_BLOCK_LENGTH, Encoding)
}
NT_STATUS := DllCall("bcrypt\BCryptDecrypt", "ptr", hKey
, "ptr", &pbInput
, "uint", cbInput
, "ptr", 0
, "ptr", (pbIV ? &pbIV : 0)
, "uint", (cbIV ? &cbIV : 0)
, "ptr", 0
, "uint", 0
, "uint*", cbOutput
, "uint", dwFlags)
if (NT_STATUS = this.STATUS_SUCCESS)
{
VarSetCapacity(pbOutput, cbOutput, 0)
NT_STATUS := DllCall("bcrypt\BCryptDecrypt", "ptr", hKey
, "ptr", &pbInput
, "uint", cbInput
, "ptr", 0
, "ptr", (pbIV ? &pbIV : 0)
, "uint", (cbIV ? &cbIV : 0)
, "ptr", &pbOutput
, "uint", cbOutput
, "uint*", cbOutput
, "uint", dwFlags)
if (NT_STATUS = this.STATUS_SUCCESS)
{
return cbOutput
}
}
return false
}
Encrypt(hKey, ByRef pbInput, cbInput, IV, BCRYPT_BLOCK_LENGTH, ByRef pbOutput, dwFlags := 0)
{
cbOutput := pbIV := cbIV := 0, Encoding := "UTF-8"
;cbInput := Crypt.Helper.StrPutVar(String, pbInput, Encoding)
if (IV != "")
{
cbIV := VarSetCapacity(pbIV, BCRYPT_BLOCK_LENGTH, 0)
StrPut(IV, &pbIV, BCRYPT_BLOCK_LENGTH, Encoding)
}
NT_STATUS := DllCall("bcrypt\BCryptEncrypt", "ptr", hKey
, "ptr", &pbInput
, "uint", cbInput
, "ptr", 0
, "ptr", (pbIV ? &pbIV : 0)
, "uint", (cbIV ? &cbIV : 0)
, "ptr", 0
, "uint", 0
, "uint*", cbOutput
, "uint", dwFlags)
if (NT_STATUS = this.STATUS_SUCCESS)
{
VarSetCapacity(pbOutput, cbOutput, 0)
NT_STATUS := DllCall("bcrypt\BCryptEncrypt", "ptr", hKey
, "ptr", &pbInput
, "uint", cbInput
, "ptr", 0
, "ptr", (pbIV ? &pbIV : 0)
, "uint", (cbIV ? &cbIV : 0)
, "ptr", &pbOutput
, "uint", cbOutput
, "uint*", cbOutput
, "uint", dwFlags)
if (NT_STATUS = this.STATUS_SUCCESS)
{
return cbOutput
}
}
return false
}
EnumAlgorithms(dwAlgOperations)
{
NT_STATUS := DllCall("bcrypt\BCryptEnumAlgorithms", "uint", dwAlgOperations
, "uint*", pAlgCount
, "ptr*", ppAlgList
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
{
addr := ppAlgList, BCRYPT_ALGORITHM_IDENTIFIER := []
loop % pAlgCount
{
BCRYPT_ALGORITHM_IDENTIFIER[A_Index, "Name"] := StrGet(NumGet(addr + A_PtrSize * 0, "uptr"), "utf-16")
BCRYPT_ALGORITHM_IDENTIFIER[A_Index, "Class"] := NumGet(addr + A_PtrSize * 1, "uint")
BCRYPT_ALGORITHM_IDENTIFIER[A_Index, "Flags"] := NumGet(addr + A_PtrSize * 1 + 4, "uint")
addr += A_PtrSize * 2
}
return BCRYPT_ALGORITHM_IDENTIFIER
}
return false
}
EnumProviders(pszAlgId)
{
NT_STATUS := DllCall("bcrypt\BCryptEnumProviders", "ptr", pszAlgId
, "uint*", pImplCount
, "ptr*", ppImplList
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
{
addr := ppImplList, BCRYPT_PROVIDER_NAME := []
loop % pImplCount
{
BCRYPT_PROVIDER_NAME.Push(StrGet(NumGet(addr + A_PtrSize * 0, "uptr"), "utf-16"))
addr += A_PtrSize
}
return BCRYPT_PROVIDER_NAME
}
return false
}
FinishHash(hHash, ByRef pbOutput, cbOutput)
{
VarSetCapacity(pbOutput, cbOutput, 0)
NT_STATUS := DllCall("bcrypt\BCryptFinishHash", "ptr", hHash
, "ptr", &pbOutput
, "uint", cbOutput
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return cbOutput
return false
}
GenerateSymmetricKey(hAlgorithm, Key, Encoding := "utf-8")
{
cbSecret := Crypt.Helper.StrPutVar(Key, pbSecret, Encoding)
NT_STATUS := DllCall("bcrypt\BCryptGenerateSymmetricKey", "ptr", hAlgorithm
, "ptr*", phKey := 0
, "ptr", 0
, "uint", 0
, "ptr", &pbSecret
, "uint", cbSecret
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return phKey
return false
}
GetProperty(hObject, pszProperty, cbOutput)
{
pcbResult := pbOutput := 0
NT_STATUS := DllCall("bcrypt\BCryptGetProperty", "ptr", hObject
, "ptr", &pszProperty
, "uint*", pbOutput
, "uint", cbOutput
, "uint*", pcbResult
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return pbOutput
return false
}
HashData(hHash, ByRef pbInput, cbInput)
{
NT_STATUS := DllCall("bcrypt\BCryptHashData", "ptr", hHash
, "ptr", &pbInput
, "uint", cbInput
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return true
return false
}
OpenAlgorithmProvider(pszAlgId, dwFlags := 0, pszImplementation := 0)
{
NT_STATUS := DllCall("bcrypt\BCryptOpenAlgorithmProvider", "ptr*", phAlgorithm := 0
, "ptr", &pszAlgId
, "ptr", pszImplementation
, "uint", dwFlags)
if (NT_STATUS = this.STATUS_SUCCESS)
return phAlgorithm
return false
}
SetProperty(hObject, pszProperty, pbInput)
{
bInput := StrLen(pbInput)
NT_STATUS := DllCall("bcrypt\BCryptSetProperty", "ptr", hObject
, "ptr", &pszProperty
, "ptr", &pbInput
, "uint", bInput
, "uint", dwFlags := 0)
if (NT_STATUS = this.STATUS_SUCCESS)
return true
return false
}
}
class Helper
{
static hCRYPT32 := DllCall("LoadLibrary", "str", "crypt32.dll", "ptr")
CryptBinaryToString(ByRef pbBinary, cbBinary, dwFlags := "BASE64")
{
static CRYPT_STRING := { "BASE64": 0x1, "BINARY": 0x2, "HEX": 0x4, "HEXRAW": 0xc }
static CRYPT_STRING_NOCRLF := 0x40000000
if (DllCall("crypt32\CryptBinaryToString", "ptr", &pbBinary
, "uint", cbBinary
, "uint", (CRYPT_STRING[dwFlags] | CRYPT_STRING_NOCRLF)
, "ptr", 0
, "uint*", pcchString := 0))
{
VarSetCapacity(pszString, pcchString << !!A_IsUnicode, 0)
if (DllCall("crypt32\CryptBinaryToString", "ptr", &pbBinary
, "uint", cbBinary
, "uint", (CRYPT_STRING[dwFlags] | CRYPT_STRING_NOCRLF)
, "ptr", &pszString
, "uint*", pcchString))
{
return StrGet(&pszString)
}
}
return false
}
CryptStringToBinary(pszString, ByRef pbBinary, dwFlags := "BASE64")
{
static CRYPT_STRING := { "BASE64": 0x1, "BINARY": 0x2, "HEX": 0x4, "HEXRAW": 0xc }
if (DllCall("crypt32\CryptStringToBinary", "ptr", &pszString
, "uint", 0
, "uint", CRYPT_STRING[dwFlags]
, "ptr", 0
, "uint*", pcbBinary := 0
, "ptr", 0
, "ptr", 0))
{
VarSetCapacity(pbBinary, pcbBinary, 0)
if (DllCall("crypt32\CryptStringToBinary", "ptr", &pszString
, "uint", 0
, "uint", CRYPT_STRING[dwFlags]
, "ptr", &pbBinary
, "uint*", pcbBinary
, "ptr", 0
, "ptr", 0))
{
return pcbBinary
}
}
return false
}
StrPutVar(String, ByRef Data, Encoding)
{
if (Encoding = "hex")
{
String := InStr(String, "0x") ? SubStr(String, 3) : String
VarSetCapacity(Data, (Length := StrLen(String) // 2), 0)
loop % Length
NumPut("0x" SubStr(String, 2 * A_Index - 1, 2), Data, A_Index - 1, "char")
return Length
}
else
{
VarSetCapacity(Data, Length := StrPut(String, Encoding) * ((Encoding = "utf-16" || Encoding = "cp1200") ? 2 : 1) - 1)
return StrPut(String, &Data, Length, Encoding)
}
}
}
class Verify
{
ChainingMode(ChainMode)
{
switch ChainMode
{
case "CBC", "ChainingModeCBC": return Crypt.Constants.BCRYPT_CHAIN_MODE_CBC
case "CFB", "ChainingModeCFB": return Crypt.Constants.BCRYPT_CHAIN_MODE_CFB
case "ECB", "ChainingModeECB": return Crypt.Constants.BCRYPT_CHAIN_MODE_ECB
default: return ""
}
}
EncryptionAlgorithm(Algorithm)
{
switch Algorithm
{
case "AES": return Crypt.Constants.BCRYPT_AES_ALGORITHM
case "DES": return Crypt.Constants.BCRYPT_DES_ALGORITHM
case "RC2": return Crypt.Constants.BCRYPT_RC2_ALGORITHM
case "RC4": return Crypt.Constants.BCRYPT_RC4_ALGORITHM
default: return ""
}
}
HashAlgorithm(Algorithm)
{
switch Algorithm
{
case "MD2": return Crypt.Constants.BCRYPT_MD2_ALGORITHM
case "MD4": return Crypt.Constants.BCRYPT_MD4_ALGORITHM
case "MD5": return Crypt.Constants.BCRYPT_MD5_ALGORITHM
case "SHA1", "SHA-1": return Crypt.Constants.BCRYPT_SHA1_ALGORITHM
case "SHA256", "SHA-256": return Crypt.Constants.BCRYPT_SHA256_ALGORITHM
case "SHA384", "SHA-384": return Crypt.Constants.BCRYPT_SHA384_ALGORITHM
case "SHA512", "SHA-512": return Crypt.Constants.BCRYPT_SHA512_ALGORITHM
default: return ""
}
}
}
; ===== CONSTANTS ===========================================================================================================
class Constants
{
static BCRYPT_ALG_HANDLE_HMAC_FLAG := 0x00000008
static BCRYPT_BLOCK_PADDING := 0x00000001
; AlgOperations flags for use with BCryptEnumAlgorithms()
static BCRYPT_CIPHER_OPERATION := 0x00000001
static BCRYPT_HASH_OPERATION := 0x00000002
static BCRYPT_ASYMMETRIC_ENCRYPTION_OPERATION := 0x00000004
static BCRYPT_SECRET_AGREEMENT_OPERATION := 0x00000008
static BCRYPT_SIGNATURE_OPERATION := 0x00000010
static BCRYPT_RNG_OPERATION := 0x00000020
static BCRYPT_KEY_DERIVATION_OPERATION := 0x00000040
; https://docs.microsoft.com/en-us/windows/win32/seccng/cng-algorithm-identifiers
static BCRYPT_3DES_ALGORITHM := "3DES"
static BCRYPT_3DES_112_ALGORITHM := "3DES_112"
static BCRYPT_AES_ALGORITHM := "AES"
static BCRYPT_AES_CMAC_ALGORITHM := "AES-CMAC"
static BCRYPT_AES_GMAC_ALGORITHM := "AES-GMAC"
static BCRYPT_DES_ALGORITHM := "DES"
static BCRYPT_DESX_ALGORITHM := "DESX"
static BCRYPT_MD2_ALGORITHM := "MD2"
static BCRYPT_MD4_ALGORITHM := "MD4"
static BCRYPT_MD5_ALGORITHM := "MD5"
static BCRYPT_RC2_ALGORITHM := "RC2"
static BCRYPT_RC4_ALGORITHM := "RC4"
static BCRYPT_RNG_ALGORITHM := "RNG"
static BCRYPT_SHA1_ALGORITHM := "SHA1"
static BCRYPT_SHA256_ALGORITHM := "SHA256"
static BCRYPT_SHA384_ALGORITHM := "SHA384"
static BCRYPT_SHA512_ALGORITHM := "SHA512"
static BCRYPT_PBKDF2_ALGORITHM := "PBKDF2"
static BCRYPT_XTS_AES_ALGORITHM := "XTS-AES"
; https://docs.microsoft.com/en-us/windows/win32/seccng/cng-property-identifiers
static BCRYPT_BLOCK_LENGTH := "BlockLength"
static BCRYPT_CHAINING_MODE := "ChainingMode"
static BCRYPT_CHAIN_MODE_CBC := "ChainingModeCBC"
static BCRYPT_CHAIN_MODE_CCM := "ChainingModeCCM"
static BCRYPT_CHAIN_MODE_CFB := "ChainingModeCFB"
static BCRYPT_CHAIN_MODE_ECB := "ChainingModeECB"
static BCRYPT_CHAIN_MODE_GCM := "ChainingModeGCM"
static BCRYPT_HASH_LENGTH := "HashDigestLength"
static BCRYPT_OBJECT_LENGTH := "ObjectLength"
}
}
--- a/Crypt.ahk
+++ b/Crypt.ahk
@@ -367,0 +368 @@
+ phHash := pbSecret := cbSecret := 0
@@ -419,0 +421 @@
+ cbOutput := pbIV := cbIV := 0, Encoding := "UTF-8"
@@ -462,0 +465 @@
+ cbOutput := pbIV := cbIV := 0, Encoding := "UTF-8"
@@ -565 +568 @@
- , "ptr*", phKey
+ , "ptr*", phKey := 0
@@ -579,0 +583 @@
+ pcbResult := pbOutput := 0
@@ -608 +612 @@
- NT_STATUS := DllCall("bcrypt\BCryptOpenAlgorithmProvider", "ptr*", phAlgorithm
+ NT_STATUS := DllCall("bcrypt\BCryptOpenAlgorithmProvider", "ptr*", phAlgorithm := 0
@@ -648 +652 @@
- , "uint*", pcchString))
+ , "uint*", pcchString := 0))
@@ -672 +676 @@
- , "uint*", pcbBinary
+ , "uint*", pcbBinary := 0
; Clear on sleep:
MasterPassword_Clear({"Sleep": true})
return ; End of auto-execute
; To create a new encrypted file containing your password
^!c::
InputBox passwd, Master Password:,,, 200, 100,,, Locale
if (!passwd)
return
MasterPassword_Create(passwd, A_AppData "\master.dat")
return
; To type your password after successfully decrypting it
^!p:: ; Via hotkey
:*X:pass\:: ; Via hotstring
SetKeyDelay 30
SendEvent % "{Text}" MasterPassword(A_AppData "\master.dat")
return
#Include %A_LineFile%\..\Crypt.ahk
; https://github.com/jNizM/AHK_CNG
#Include %A_LineFile%\..\MasterPassword.ahk
; Version: 2023.01.06.1
; Information: https://redd.it/1051mkc
MasterPassword(Path) {
static decrypted := ""
if (!Path)
return decrypted := ""
if (decrypted)
return decrypted
try
FileRead data, % Path
catch
throw Exception("Couldn't read password file.", -1, Path)
data := StrSplit(data, "|")
encrypted := data[1]
iterations := data[2]
loop 3 {
InputBox key, Encryption Key:,, Hide, 200, 100,,, Locale
if (ErrorLevel)
Exit
if (!key)
continue
try {
salt := MasterPassword_Salt(key)
key := Crypt.Hash.PBKDF2("SHA512", key, salt, iterations, 512)
decrypted := Crypt.Decrypt.String("AES", "CBC", encrypted, key)
return decrypted
}
}
MsgBox 0x40010, Error, Password couldn't be decrypted.
Exit
}
; Options := {}
; Options.Inactive := int ; In minutes
; Options.Lid := bool ; On lid close
; Options.Lock := bool ; On lock screen
; Options.Sleep := bool ; On system sleep
MasterPassword_Clear(Options) {
static timer := "", a := "", b := ""
if (IsObject(timer)) {
SetTimer % timer, Delete
timer := ""
}
if (IsObject(a))
OnMessage(0x0218, a, 0), a := ""
if (IsObject(b))
OnMessage(0x02B1, b, 0), b := ""
if (!IsObject(Options))
return
if (Options.Inactive ~= "^\d+$") {
if (Options.Inactive) {
ms := 1000 * 60 * Options.Inactive
timer := Func("MasterPassword_Timer").Bind(ms)
SetTimer % timer, % 1000 * 10
}
}
; WM_POWERBROADCAST
if (Options.Lid = true || Options.Sleep = true) {
VarSetCapacity(GUID_LIDSWITCH_STATE_CHANGE, 16, 0)
NumPut(0xBA3E0F4D,GUID_LIDSWITCH_STATE_CHANGE, 0, "UInt")
NumPut(0x4094B817,GUID_LIDSWITCH_STATE_CHANGE, 4, "UInt")
NumPut(0x63D5D1A2,GUID_LIDSWITCH_STATE_CHANGE, 8, "UInt")
NumPut(0xF3A0E679,GUID_LIDSWITCH_STATE_CHANGE, 12, "UInt")
DllCall("User32\RegisterPowerSettingNotification"
, "UInt",A_ScriptHwnd
, "Ptr",&GUID_LIDSWITCH_STATE_CHANGE
, "UInt",0)
if (IsObject(a)) {
OnMessage(0x0218, a, 0)
a := ""
}
a := Func("MasterPassword_Monitor").Bind("A")
OnMessage(0x0218, a)
}
; WM_WTSSESSION_CHANGE
if (Options.Lock = true) {
DllCall("Wtsapi32\WTSRegisterSessionNotification"
, "Ptr",A_ScriptHwnd
, "UInt",1)
if (IsObject(b)) {
OnMessage(0x02B1, b, 0)
b := ""
}
b := Func("MasterPassword_Monitor").Bind("B")
OnMessage(0x02B1, b)
}
}
MasterPassword_Create(Pass, Path) {
; Dynamically generated salt
salt := MasterPassword_Salt(Pass)
; Calculate iterations per second
tc := A_TickCount, iterations := 100000
Crypt.Hash.PBKDF2("SHA512", Pass, salt, iterations, 512)
elapsed := A_TickCount - tc
iterations := Ceil(1000 * iterations / elapsed)
; Derive key
key := Crypt.Hash.PBKDF2("SHA512", Pass, salt, iterations, 512)
; Encrypt password with AES CBC
encrypted := Crypt.Encrypt.String("AES", "CBC", Pass, key)
; Save it alongside the number of number of iterations for the key
if (!FileOpen(Path, 0x1, "CP1252").Write(encrypted "|" iterations))
throw Exception("Couldn't write to " Path, -1)
}
MasterPassword_Monitor(Type, wParam, lParam, Msg) {
if (Type = "A" && Msg = 0x0218 && wParam = 0x4) ; Lid/Sleep
|| (Type = "B" && Msg = 0x02B1 && wParam = 0x7) ; Lock
MasterPassword("")
}
MasterPassword_Salt(String) {
strLen := StrLen(String)
seed := DllCall("Ntdll\RtlComputeCrc32"
, "Ptr",0
, "AStr",String
, "UInt",strLen
, "UInt")
Random ,, % seed
salt := ""
loop % strLen {
Random r, 0x20, 0xFFFF
salt .= Chr(r)
}
Random ,, % A_Now
salt := Crypt.Hash.String("SHA512", salt)
return salt
}
MasterPassword_Timer(ms) {
if (A_TimeIdle >= ms)
MasterPassword("")
}

Storing sensitive information (passwords) within AHK

Source: anonymous on Reddit

AutoHotkey is not a replacement for a password manager!!!

At least not without investing time and effort into making a proper password manager out of AHK (and that is completely outside the scope of this post, plus there are already many comprehensive solutions available).

The next thing is to acknowledge pretty obvious points that everyone must be aware of:

  • Everything can be broken.
  • Nothing is ever 100% secure.

But, depending on what you do to protect your information, is how unattractive it becomes for someone looking for whatever can be fished. It is not the same to have a file on the desktop named bank password.txt with your credentials on the clear in there; than have an inconspicuous filename with multi-layered encryption and brute force/dictionary attacks slowed down by key derivation.

Security and cryptography both are really vast and complex topics, if you're interested in them, there are countless communities better suited for that. Here I'm just gonging to demonstrate how to safely have available in AHK a single* password** to be used for automation purposes.

^* It can be adapted to pretty much anything that needs to be secured, not just a single password.^
^** Checkout GeekDude's comment as there you have an OS built-in option.^

Last note before we dig in; if you start an argument with: "If an attacker gains access...", then we are not ever going to achieve anything, you are already exposed at that point; so it is trivial for said attacker to do pretty much anything. The fault is not AHK, the fault was many layers of security before. With or without AHK, the attacker can easily have what it pleases (browsers are a gold mine and keyloggers are meh to write and disguise).

AHK and your Master Password

^After the longest intro ever...^

If you are already using a password manager and following the basic principle of having a strong/complex and lengthy master password, then you are presented with the incredibly cumbersome task of typing it over and over again.

And AHK is about automation and typing for you, right?

I'll present here a secure flow similar to what has been my daily driver for a really long time. At least as secure as cryptography goes, you still have to account for human error and how vulnerable is your system/network (at the end, I'll go about a few points on how to tighten the security a little).

This is what NEVER should be done:

^!p::               ; ← NEVER
    Send qwerty123  ; ←  DO
return              ; ← THIS!

On top of having what is one of the top worst passwords, having it as clear text in a script is against the most basic common sense.

AutoHotkey scripts are not protected in any way*; scripts are NOT compiled, AHK is an interpreted language, and they are converted to an executable. The process is just adding the plain text script contents as a resource of an executable, then the contents are executed same as when reading a file.

* AHK_H has the option to do so, but is not without some extra work.

Here you have a better approach with the same result (is an oversimplification for demonstrative purposes):

^!p::Send % MasterPassword(A_MyDocuments "\master.dat")

MasterPassword(Path) {
    static decrypted := ""
    if (decrypted)
        return decrypted
    FileRead encrypted, % Path
    loop 3 {
        InputBox key, Encryption Key:,, Hide, 200, 100
        decrypted := Decrypt(encrypted, key)
        return decrypted
    }
    MsgBox 0x40010, Error, Password couldn't be decrypted.
}

What the above does?

First, the password is not stored in the script but loaded from an encrypted file called master.dat; upon the first usage, you are prompted to provide the encryption key and have the 3 standard opportunities to decrypt your password. When successful, the password is kept unencrypted in memory) and ready for later usage.

The result is as secure as the encryption method you used to encrypt your password. And yes, you can safely encrypt your master password with (drumroll)... your master password

!!! ergo, you don't have to memorize yet another password, avoiding: the password to access the password used for your passwords.

Why is secure and how it works?

Now, let's go over the worst-case scenario that will never happen:

  • You use a laptop.
  • Your laptop is stolen.
  • Boot is not password protected.
  • Storage is not encrypted.
  • Windows account doesn't have a password.
  • The robber is well-versed in AHK.

Now, the robber turns on the laptop and goes all the way to the desktop as there is nothing to stop him... then sees that AutoHotkey is installed and that a script is loaded on startup; proceeds to meticulously examine the script and sees that there's a function to type a master password.

You are in serious problems. But for that insecure system, and NOT because you have your master password accessible to the script. In any case, if you have the password encrypted, unless the robber knows the decryption key he won't be able to get the password.

Let's tackle the next possible argument: What if the password is already unencrypted? If the laptop, on top of being this insecure, is taken away by the robber while it is turned on and the robber keeps it like that, then the password is there for the taking... right? At that point, in that ludicrous scenario, is more likely to dump the whole contents of the password manager; again, AHK is not the weak link.

But even accounting for that unrealistic scenario, you can adjust how much time and under which conditions the password is kept in memory with any combination of the following:

  • Manual removal: whenever you feel like it.
  • Computer sleep: when putting the computer to sleep (commute?).
  • Windows Lock Screen: built-in OS locking mechanism (coffee break?).
  • Lid close: if either the computer locks or not (docking station).
  • Inactivity period: not having physical contact with the PC.

So, there you have it; you can actually have your master password accessible to AHK without posing an unnecessary risk.


I'm not gonna add the code/examples here as Reddit lacks syntax highlighting and this much code makes no sense in a post, I rather add the code with examples in a gist alongside a proper encryption/decryption technique that includes key derivation.

I use the password itself as the decryption key to simplify the example, but the data to be encrypted, and the key can be different and of course, it doesn't need to be just a single password. Another example would be to have an encrypted CSV file and the data loaded into an object.

Skip the next section if you know what key derivation is, how it works and how it helps to making brute force attacks harder to success.

Key derivation

If you don't know what key derivation is and how it can help: is a technique used to slow down brute force and dictionary attacks by sequentially deriving your initial encryption key and using not your key, but the derived key of the last iteration; as a result, the decryption process is slower (but only for people that doesn't know the actual key and attempts to brute-force it).

Example:

You have data protected with a 4-digit PIN (please don't), then there are up to 9,999 possible combinations to decrypt it; any modern computer will take less than a second for those 9,999 attempts unless key derivation is used.

Say a key derivation process consisting of 500,000 iterations is used, and it adds a second per attempt. Meaning that instead of cracking the PIN in under one second, it will take up to 2 hours, 46 minutes and 38 seconds: one second for each attempt (if the PIN is actually 9999). Bear in mind that different implementation and hardware have different speeds... 500,000 iterations might not be a whole second, yet it is effectively half a million times slower whatever the speed.

That's why you're encouraged to use long passwords and a big alphabet (ie, lower/upper case with numbers and symbols). The result is that casually trying to decrypt data is not worth the time, effort, and cost (CPU processing is costly). Hence, this is perfectly suited and more than adequate for most people (nuclear launch codes protection not included).

Full working example

The files in this gist have all that's needed to get the flow I described in the post. To make life easier, I'm using jNizM's AHK_CNG class as it uses BCrypt rather than the CryptoAPI. Plus, it has different output options that simplifies data read/write. With that being said, you can use any method you trust (AGE or GnuPG are other examples).

  • example.ahk: how to use it in your script.
  • Lib\Crypt.ahk: dependency
  • Lib\MasterPassword.ahk: the actual implementation.

For the key derivation, a dynamic salt of the same length of the password is generated. It is also estimated how many derivations can be made in a second, that calculation is then used.

Some basic security tips

This is not the place to look for security advice, but is relevant to the topic. Also, these tips can be expanded indefinitely and in so many cases are not enough (or too much)... please take them as intended: a reminder that boosting the security of your system doesn't mean extravagances or spending on costly software/hardware, and more especially that security is not exclusively for M.I.B., S.T.A.R., S.H.I.E.L.D., and the likes.

  • BIOS password protection: this is the first line to keep privy eyes from your system (if your household has them).
  • Encrypt your storage: Windows has options to do so (EFS/BitLocker) and if you don't like them or are not available to you, there are free 3rd parties like VeraCrypt or Cryptomator; the latter is awesome with Cloud storage and have mobile options.
  • Password-protect the Windows account: doesn't protect data but has its merits as it doesn't let anyone around to snoop (coworkers trying to prank, for example).
  • Physical and software Firewalls: VPNs are mostly snake oil, properly configured firewalls can be more effective than other networking security solutions. Virtually every modem/router has one, and Window's built-in on whitelist mode is not half-bad.
  • Kensington locks: if your computer can be snatched, why not? Nice when you move around a lot, totally worth it given how cheap they are.

The most beneficial might be storage encryption. With how powerful and fast consumer hardware is nowadays, transparent/on-the-fly encryption is pretty attainable to anyone. Having at least a partition with the personal information encrypted would be my bare minimum. I mean, there's no need to encrypt your multi-TB game library, but personal stuff shouldn't be left in the clear.

Closing note

Are you really sure it is safe? YES.

If you use only upper/lower, digits, numbers, and symbols a rather common 12-character password with those 94 characters will have the following number of combinations:

106,890,007,738,661,187,092,480 ; Almost 107 sextillions

Meaning a brute force attack needs to go over those, if key derivation is used, well... is just nuts.

But there's more! If also Unicode characters are added, the attack needs to go all the way up to the "Symbols for Legacy Computing" (130,015 characters). I haven't been able to locate a calculator that doesn't generate an error for this absurd number of combinations, the entropy alone is about 466,096 bits (for context, a password entropy of 96 bits is considered "good enough").


EDIT: G33kDude's reference to the Credential Manager API. (code below)

Last update: 2023/01/10

if !CredWrite("AHK_CredForScript1", "SomeUsername", "SomePassword")
  MsgBox failed to write cred

if (cred := CredRead("AHK_CredForScript1"))
  MsgBox % cred.name "," cred.username "," cred.password
else
  MsgBox Cred not found

if !CredDelete("AHK_CredForScript1")
  MsgBox Failed to delete cred

if (cred := CredRead("AHK_CredForScript1"))
  MsgBox % cred.name "," cred.username "," cred.password
else
  MsgBox Cred not found

CredWrite(name, username, password)
{
  VarSetCapacity(cred, 24 + A_PtrSize * 7, 0)
  cbPassword := StrLen(password)*2
  NumPut(1         , cred,  4+A_PtrSize*0, "UInt") ; Type = CRED_TYPE_GENERIC
  NumPut(&name     , cred,  8+A_PtrSize*0, "Ptr")  ; TargetName = name
  NumPut(cbPassword, cred, 16+A_PtrSize*2, "UInt") ; CredentialBlobSize
  NumPut(&password , cred, 16+A_PtrSize*3, "UInt") ; CredentialBlob
  NumPut(3         , cred, 16+A_PtrSize*4, "UInt") ; Persist = CRED_PERSIST_ENTERPRISE (roam across domain)
  NumPut(&username , cred, 24+A_PtrSize*6, "Ptr")  ; UserName
  return DllCall("Advapi32.dll\CredWriteW"
  , "Ptr", &cred ; [in] PCREDENTIALW Credential
  , "UInt", 0    ; [in] DWORD        Flags
  , "UInt") ; BOOL
}

CredDelete(name)
{
  return DllCall("Advapi32.dll\CredDeleteW"
  , "WStr", name ; [in] LPCWSTR TargetName
  , "UInt", 1    ; [in] DWORD   Type,
  , "UInt", 0    ; [in] DWORD   Flags
  , "UInt") ; BOOL
}

CredRead(name)
{
  DllCall("Advapi32.dll\CredReadW"
  , "Str", name   ; [in]  LPCWSTR      TargetName
  , "UInt", 1     ; [in]  DWORD        Type = CRED_TYPE_GENERIC (https://learn.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala)
  , "UInt", 0     ; [in]  DWORD        Flags
  , "Ptr*", pCred ; [out] PCREDENTIALW *Credential
  , "UInt") ; BOOL
  if !pCred
      return
  name := StrGet(NumGet(pCred + 8 + A_PtrSize * 0, "UPtr"), 256, "UTF-16")
  username := StrGet(NumGet(pCred + 24 + A_PtrSize * 6, "UPtr"), 256, "UTF-16")
  len := NumGet(pCred + 16 + A_PtrSize * 2, "UInt")
  password := StrGet(NumGet(pCred + 16 + A_PtrSize * 3, "UPtr"), len/2, "UTF-16")
  DllCall("Advapi32.dll\CredFree", "Ptr", pCred)
  return {"name": name, "username": username, "password": password}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment