diff --git a/.gitignore b/.gitignore index b2dc6d9..cf5c341 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -release/* +# Folders +.vscode +dev +release + +# Types +*.code-workspace diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 0000000..5c98251 --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,47 @@ +YYYY/MM/DD - MAJOR.MINOR.RELEASE +———————————————————————————————— +[+] = Added +[*] = Changed +[-] = Removed +[^] = Moved +[=] = Unchanged +[!] = Fix / Security + + +2021/05/21 - 1.0.0 +—————————————————— +[!] Less fatal errors +[!] Full Unicode support (#2) +[!] Performance improvements +[!] Support for passwords with spaces (#3) +[-] Sync at login +[-] oathtool for TOTP +[+] Browser support +[+] Keepass placeholders +[+] Unlock via TOTP +[+] Unlock/Login via PIN +[+] favicon support for account selection +[+] Major Browsers Support +[+] Multiple account support per service +[+] Scheduled sync +[+] Automatic logout +[+] Secure Password Generator +[+] Two-Channel Auto-Type Obfuscation +[+] Autorun at system startup +[+] Ability to install and run with UI Access +[+] Update checking +[+] TOTP and Username-only hotkeys +[^] AutoHotkey version 1.1.33.09 +[^] Bitwarden CLI version >= 1.11.0 + + +2020/04/07 - 0.0.2 +—————————————————— +[!] Issue #1 +[+] Build script +[=] Bitwarden CLI version >= 1.9.0 + + +2020/03/24 - 0.0.1 +—————————————————— +[+] Initial release diff --git a/Lib/Acc.ahk b/Lib/Acc.ahk new file mode 100644 index 0000000..804033e --- /dev/null +++ b/Lib/Acc.ahk @@ -0,0 +1,46 @@ + +; Improved Core of the Acc.ahk Standard Library +; http://autohotkey.com/board/topic/77303-/?p=491516 +; https://gist.github.com/tmplinshi/2d3a1deb693e72789d8f + +Acc_ObjectFromWindow(hWnd, idObject := -4) +{ + static h := DllCall("Kernel32\LoadLibrary", "Str","oleacc.dll", "Ptr") + pAcc := "" + if !DllCall("oleacc\AccessibleObjectFromWindow" + , "Ptr" ,hWnd + , "UInt",idObject &= 0xFFFFFFFF + , "Ptr" ,-VarSetCapacity(IID, 16) + NumPut(idObject==0xFFFFFFF0?0x46000000000000C0:0x719B3800AA000C81, NumPut(idObject==0xFFFFFFF0?0x0000000000020400:0x11CF3C3D618736E0, IID, "Int64"), "Int64") + , "Ptr*",pAcc) + return ComObjEnwrap(9, pAcc, 1) +} + +Acc_Children(Acc) +{ + static procAddr := DllCall("Kernel32\GetProcAddress" + , "Ptr" ,DllCall("Kernel32\GetModuleHandle", "Str","oleacc.dll", "Ptr") + , "AStr","AccessibleChildren" + , "Ptr" ) + if ComObjType(Acc, "Name") != "IAccessible" + throw Exception("Invalid IAccessible Object", -1) + children := [] + cChildren := Acc.accChildCount + VarSetCapacity(varChildren, cChildren * (8 + 2 * A_PtrSize), 0) + if DllCall(procAddr + , "Ptr" ,ComObjValue(Acc) + , "Int" ,0 + , "Int" ,cChildren + , "Ptr" ,&varChildren + , "Int*",cChildren) + throw Exception("AccessibleChildren DllCall Failed", -1) + loop % cChildren + i := (A_Index - 1) * (A_PtrSize * 2 + 8) + 8 + , child := NumGet(varChildren, i) + , children.Insert(NumGet(varChildren, i - 8) = 9 ? Acc_Query(child) : child) + return children.MaxIndex() ? children : false +} + +Acc_Query(Acc) +{ + try return ComObj(9, ComObjQuery(Acc, "{618736e0-3c3d-11cf-810c-00aa00389b71}"), 1) +} diff --git a/Lib/Crypt.ahk b/Lib/Crypt.ahk new file mode 100644 index 0000000..c741af4 --- /dev/null +++ b/Lib/Crypt.ahk @@ -0,0 +1,817 @@ +; =============================================================================================================================== +; 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") + { + if (hmac) + cbSecret := Crypt.helper.StrPutVar(hmac, pbSecret, encoding) + phHash := "" + 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) + { + VarSetCapacity(pbInput, cbInput, 0) + DllCall("msvcrt\memcpy", "ptr", &pbInput, "ptr", &String, "ptr", cbInput) + + if (IV != "") + { + Encoding := "UTF-8" + cbIV := VarSetCapacity(pbIV, BCRYPT_BLOCK_LENGTH, 0) + StrPut(IV, &pbIV, BCRYPT_BLOCK_LENGTH, Encoding) + } + cbOutput := pbIV := cbIV := "" + 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) + { + ;cbInput := Crypt.Helper.StrPutVar(String, pbInput, Encoding) + + if (IV != "") + { + Encoding := "UTF-8" + cbIV := VarSetCapacity(pbIV, BCRYPT_BLOCK_LENGTH, 0) + StrPut(IV, &pbIV, BCRYPT_BLOCK_LENGTH, Encoding) + } + cbOutput := pbIV := cbIV := "" + 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") + { + phKey := "" + cbSecret := Crypt.Helper.StrPutVar(Key, pbSecret, Encoding) + NT_STATUS := DllCall("bcrypt\BCryptGenerateSymmetricKey", "ptr", hAlgorithm + , "ptr*", phKey + , "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 := "" + 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) + { + phAlgorithm := "" + NT_STATUS := DllCall("bcrypt\BCryptOpenAlgorithmProvider", "ptr*", phAlgorithm + , "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 + + pcchString := "" + if (DllCall("crypt32\CryptBinaryToString", "ptr", &pbBinary + , "uint", cbBinary + , "uint", (CRYPT_STRING[dwFlags] | CRYPT_STRING_NOCRLF) + , "ptr", 0 + , "uint*", pcchString)) + { + 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 } + pcbBinary := "" + if (DllCall("crypt32\CryptStringToBinary", "ptr", &pszString + , "uint", 0 + , "uint", CRYPT_STRING[dwFlags] + , "ptr", 0 + , "uint*", pcbBinary + , "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" + } +} diff --git a/Lib/JSON.ahk b/Lib/JSON.ahk new file mode 100644 index 0000000..5574b42 --- /dev/null +++ b/Lib/JSON.ahk @@ -0,0 +1,374 @@ +/** + * Lib: JSON.ahk + * JSON lib for AutoHotkey. + * Version: + * v2.1.3 [updated 04/18/2016 (MM/DD/YYYY)] + * License: + * WTFPL [http://wtfpl.net/] + * Requirements: + * Latest version of AutoHotkey (v1.1+ or v2.0-a+) + * Installation: + * Use #Include JSON.ahk or copy into a function library folder and then + * use #Include + * Links: + * GitHub: - https://github.com/cocobelgica/AutoHotkey-JSON + * Forum Topic - http://goo.gl/r0zI8t + * Email: - cocobelgica gmail com + */ + + +/** + * Class: JSON + * The JSON object contains methods for parsing JSON and converting values + * to JSON. Callable - NO; Instantiable - YES; Subclassable - YES; + * Nestable(via #Include) - NO. + * Methods: + * Load() - see relevant documentation before method definition header + * Dump() - see relevant documentation before method definition header + */ +class JSON +{ + /** + * Method: Load + * Parses a JSON string into an AHK value + * Syntax: + * value := JSON.Load( text [, reviver ] ) + * Parameter(s): + * value [retval] - parsed value + * text [in, ByRef] - JSON formatted string + * reviver [in, opt] - function object, similar to JavaScript's + * JSON.parse() 'reviver' parameter + */ + class Load extends JSON.Functor + { + Call(self, ByRef text, reviver:="") + { + this.rev := IsObject(reviver) ? reviver : false + ; Object keys(and array indices) are temporarily stored in arrays so that + ; we can enumerate them in the order they appear in the document/text instead + ; of alphabetically. Skip if no reviver function is specified. + this.keys := this.rev ? {} : false + + static quot := Chr(34), bashq := "\" . quot + , json_value := quot . "{[01234567890-tfn" + , json_value_or_array_closing := quot . "{[]01234567890-tfn" + , object_key_or_object_closing := quot . "}" + + key := "" + is_key := false + root := {} + stack := [root] + next := json_value + pos := 0 + + while ((ch := SubStr(text, ++pos, 1)) != "") { + if InStr(" `t`r`n", ch) + continue + if !InStr(next, ch, 1) + this.ParseError(next, text, pos) + + holder := stack[1] + is_array := holder.IsArray + + if InStr(",:", ch) { + next := (is_key := !is_array && ch == ",") ? quot : json_value + + } else if InStr("}]", ch) { + ObjRemoveAt(stack, 1) + next := stack[1]==root ? "" : stack[1].IsArray ? ",]" : ",}" + + } else { + if InStr("{[", ch) { + ; Check if Array() is overridden and if its return value has + ; the 'IsArray' property. If so, Array() will be called normally, + ; otherwise, use a custom base object for arrays + static json_array := Func("Array").IsBuiltIn || ![].IsArray ? {IsArray: true} : 0 + + ; sacrifice readability for minor(actually negligible) performance gain + (ch == "{") + ? ( is_key := true + , value := {} + , next := object_key_or_object_closing ) + ; ch == "[" + : ( value := json_array ? new json_array : [] + , next := json_value_or_array_closing ) + + ObjInsertAt(stack, 1, value) + + if (this.keys) + this.keys[value] := [] + + } else { + if (ch == quot) { + i := pos + while (i := InStr(text, quot,, i+1)) { + value := StrReplace(SubStr(text, pos+1, i-pos-1), "\\", "\u005c") + + static tail := A_AhkVersion<"2" ? 0 : -1 + if (SubStr(value, tail) != "\") + break + } + + if (!i) + this.ParseError("'", text, pos) + + value := StrReplace(value, "\/", "/") + , value := StrReplace(value, bashq, quot) + , value := StrReplace(value, "\b", "`b") + , value := StrReplace(value, "\f", "`f") + , value := StrReplace(value, "\n", "`n") + , value := StrReplace(value, "\r", "`r") + , value := StrReplace(value, "\t", "`t") + + pos := i ; update pos + + i := 0 + while (i := InStr(value, "\",, i+1)) { + if !(SubStr(value, i+1, 1) == "u") + this.ParseError("\", text, pos - StrLen(SubStr(value, i+1))) + + uffff := Abs("0x" . SubStr(value, i+2, 4)) + if (A_IsUnicode || uffff < 0x100) + value := SubStr(value, 1, i-1) . Chr(uffff) . SubStr(value, i+6) + } + + if (is_key) { + key := value, next := ":" + continue + } + + } else { + value := SubStr(text, pos, i := RegExMatch(text, "[\]\},\s]|$",, pos)-pos) + + static number := "number", integer :="integer" + if value is %number% + { + if value is %integer% + value += 0 + } + else if (value == "true" || value == "false") + value := %value% + 0 + else if (value == "null") + value := "" + else + ; we can do more here to pinpoint the actual culprit + ; but that's just too much extra work. + this.ParseError(next, text, pos, i) + + pos += i-1 + } + + next := holder==root ? "" : is_array ? ",]" : ",}" + } ; If InStr("{[", ch) { ... } else + + is_array? key := ObjPush(holder, value) : holder[key] := value + + if (this.keys && this.keys.HasKey(holder)) + this.keys[holder].Push(key) + } + + } ; while ( ... ) + + return this.rev ? this.Walk(root, "") : root[""] + } + + ParseError(expect, ByRef text, pos, len:=1) + { + static quot := Chr(34), qurly := quot . "}" + + line := StrSplit(SubStr(text, 1, pos), "`n", "`r").Length() + col := pos - InStr(text, "`n",, -(StrLen(text)-pos+1)) + msg := Format("{1}`n`nLine:`t{2}`nCol:`t{3}`nChar:`t{4}" + , (expect == "") ? "Extra data" + : (expect == "'") ? "Unterminated string starting at" + : (expect == "\") ? "Invalid \escape" + : (expect == ":") ? "Expecting ':' delimiter" + : (expect == quot) ? "Expecting object key enclosed in double quotes" + : (expect == qurly) ? "Expecting object key enclosed in double quotes or object closing '}'" + : (expect == ",}") ? "Expecting ',' delimiter or object closing '}'" + : (expect == ",]") ? "Expecting ',' delimiter or array closing ']'" + : InStr(expect, "]") ? "Expecting JSON value or array closing ']'" + : "Expecting JSON value(string, number, true, false, null, object or array)" + , line, col, pos) + + static offset := A_AhkVersion<"2" ? -3 : -4 + throw Exception(msg, offset, SubStr(text, pos, len)) + } + + Walk(holder, key) + { + value := holder[key] + if IsObject(value) { + for i, k in this.keys[value] { + ; check if ObjHasKey(value, k) ?? + v := this.Walk(value, k) + if (v != JSON.Undefined) + value[k] := v + else + ObjDelete(value, k) + } + } + + return this.rev.Call(holder, key, value) + } + } + + /** + * Method: Dump + * Converts an AHK value into a JSON string + * Syntax: + * str := JSON.Dump( value [, replacer, space ] ) + * Parameter(s): + * str [retval] - JSON representation of an AHK value + * value [in] - any value(object, string, number) + * replacer [in, opt] - function object, similar to JavaScript's + * JSON.stringify() 'replacer' parameter + * space [in, opt] - similar to JavaScript's JSON.stringify() + * 'space' parameter + */ + class Dump extends JSON.Functor + { + Call(self, value, replacer:="", space:="") + { + this.rep := IsObject(replacer) ? replacer : "" + + this.gap := "" + if (space) { + static integer := "integer" + if space is %integer% + Loop, % ((n := Abs(space))>10 ? 10 : n) + this.gap .= " " + else + this.gap := SubStr(space, 1, 10) + + this.indent := "`n" + } + + return this.Str({"": value}, "") + } + + Str(holder, key) + { + value := holder[key] + + if (this.rep) + value := this.rep.Call(holder, key, ObjHasKey(holder, key) ? value : JSON.Undefined) + + if IsObject(value) { + ; Check object type, skip serialization for other object types such as + ; ComObject, Func, BoundFunc, FileObject, RegExMatchObject, Property, etc. + static type := A_AhkVersion<"2" ? "" : Func("Type") + if (type ? type.Call(value) == "Object" : ObjGetCapacity(value) != "") { + if (this.gap) { + stepback := this.indent + this.indent .= this.gap + } + + is_array := value.IsArray + ; Array() is not overridden, rollback to old method of + ; identifying array-like objects. Due to the use of a for-loop + ; sparse arrays such as '[1,,3]' are detected as objects({}). + if (!is_array) { + for i in value + is_array := i == A_Index + until !is_array + } + + str := "" + if (is_array) { + Loop, % value.Length() { + if (this.gap) + str .= this.indent + + v := this.Str(value, A_Index) + str .= (v != "") ? v . "," : "null," + } + } else { + colon := this.gap ? ": " : ":" + for k in value { + v := this.Str(value, k) + if (v != "") { + if (this.gap) + str .= this.indent + + str .= this.Quote(k) . colon . v . "," + } + } + } + + if (str != "") { + str := RTrim(str, ",") + if (this.gap) + str .= stepback + } + + if (this.gap) + this.indent := stepback + + return is_array ? "[" . str . "]" : "{" . str . "}" + } + + } else ; is_number ? value : "value" + return ObjGetCapacity([value], 1)=="" ? value : this.Quote(value) + } + + Quote(string) + { + static quot := Chr(34), bashq := "\" . quot + + if (string != "") { + string := StrReplace(string, "\", "\\") + ; , string := StrReplace(string, "/", "\/") ; optional in ECMAScript + , string := StrReplace(string, quot, bashq) + , string := StrReplace(string, "`b", "\b") + , string := StrReplace(string, "`f", "\f") + , string := StrReplace(string, "`n", "\n") + , string := StrReplace(string, "`r", "\r") + , string := StrReplace(string, "`t", "\t") + + static rx_escapable := A_AhkVersion<"2" ? "O)[^\x20-\x7e]" : "[^\x20-\x7e]" + while RegExMatch(string, rx_escapable, m) + string := StrReplace(string, m.Value, Format("\u{1:04x}", Ord(m.Value))) + } + + return quot . string . quot + } + } + + /** + * Property: Undefined + * Proxy for 'undefined' type + * Syntax: + * undefined := JSON.Undefined + * Remarks: + * For use with reviver and replacer functions since AutoHotkey does not + * have an 'undefined' type. Returning blank("") or 0 won't work since these + * can't be distnguished from actual JSON values. This leaves us with objects. + * Replacer() - the caller may return a non-serializable AHK objects such as + * ComObject, Func, BoundFunc, FileObject, RegExMatchObject, and Property to + * mimic the behavior of returning 'undefined' in JavaScript but for the sake + * of code readability and convenience, it's better to do 'return JSON.Undefined'. + * Internally, the property returns a ComObject with the variant type of VT_EMPTY. + */ + Undefined[] + { + get { + static empty := {}, vt_empty := ComObject(0, &empty, 1) + return vt_empty + } + } + + class Functor + { + __Call(method, ByRef arg, args*) + { + ; When casting to Call(), use a new instance of the "function object" + ; so as to avoid directly storing the properties(used across sub-methods) + ; into the "function object" itself. + if IsObject(method) + return (new this).Call(method, arg, args*) + else if (method == "") + return (new this).Call(arg, args*) + } + } +} diff --git a/Lib/autoLock.ahk b/Lib/autoLock.ahk new file mode 100644 index 0000000..67e8eb6 --- /dev/null +++ b/Lib/autoLock.ahk @@ -0,0 +1,14 @@ + +autoLock(idle) +{ + if !mins := 1000 * 60 * idle + return + fn := Func("autoLock_timer").Bind(mins) + SetTimer % fn, % 1000 * 30 +} + +autoLock_timer(mins) +{ + if !isLocked && A_TimeIdle >= mins + toggleLock(true) +} diff --git a/Lib/autoLogout.ahk b/Lib/autoLogout.ahk new file mode 100644 index 0000000..e9b37ec --- /dev/null +++ b/Lib/autoLogout.ahk @@ -0,0 +1,14 @@ + +autoLogout(idle) +{ + if !mins := 1000 * 60 * idle + return + fn := Func("autoLogout_timer").Bind(mins) + SetTimer % fn, % 1000 * 30 +} + +autoLogout_timer(mins) +{ + if isLogged && (A_TimeIdle >= mins) + toggleLogin() +} diff --git a/Lib/autoType.ahk b/Lib/autoType.ahk new file mode 100644 index 0000000..02dd20a --- /dev/null +++ b/Lib/autoType.ahk @@ -0,0 +1,124 @@ + +autoType(entry, mode) +{ + + ; Generate TOTP + if entry.otpauth && mode ~= "default|totp" + { + entry.totp := totp(entry.otpauth) + if (entry.totp && mode = "default" && INI.GENERAL.totp != "Hide") + { + if INI.GENERAL.totp + Clipboard := entry.totp + TrayTip % appTitle, % "TOTP: " formatOtp(entry.totp), 10, 0x20 + } + } + + ; TCATO + switch entry.tcato + { + case "on" : useTCATO := true + case "off": useTCATO := false + default: + useTCATO := INI.TCATO.use + } + entry.Delete("tcato") ; To be used as placeholder + + ; Wait for keyUp + pressing := true + while pressing + { + pressing := false + loop 0xFF + { + vk := "vk" Format("{:03X}", A_Index) + if GetKeyState(vk, "P") + pressing := true + } + } + + ; Perform + BlockInput On ; Only works with UI Access. + p := 1 + while p := RegExMatch(entry.sequence, "[^{}]+|{[^{}]+}", match, p) + p += autoType_part(match, entry, useTCATO) + BlockInput Off +} + +autoType_part(part, entry, ByRef useTCATO) +{ + regex := "iS){(?\w+)(\s)?(?(?(\s)?[^ }]*)(\s)?(?(\s+)?[^}]*)?)}" + RegExMatch(part, regex, $) + + if ($ph = "AppActivate") + { + if SubStr($args, -3) = ".exe" + $args := "ahk_exe " $args + w := WinExist($args), i := 1 + while w && !WinActive() && i++ < 5 + WinActivate + } + else if ($ph = "Beep") + { + test := $arg1 $arg2 + if test is digit + SoundBeep % $arg1, % $arg2 + } + else if ($ph = "Clipboard") + { + if StrLen($args) + Clipboard := $args + else + Send % "{Raw}" Clipboard + } + else if ($ph = "ClearField") + { + Send ^a{Delete} + } + else if ($ph = "Delay") + { + delay := Trim($arg1) + if delay is number + Sleep % delay + } + else if ($ph = "SmartTab") + { + if !A_CaretX || !A_CaretY + Send {Tab} + else + { + loop 5 + { + Send {Tab}{Right} + Sleep 250 + if A_CaretX || A_CaretY + break + } + if !A_CaretX || !A_CaretY ; Not found + { + Send {Shift Down}{Tab 4}{Shift Up} + Exit ; Stop auto-typing + } + } + } + else if ($ph = "TCATO") + { + if ($arg1 = "on") + useTCATO := 1 + else if ($arg1 = "off") + useTCATO := 0 + else if !$arg1 + useTCATO ^= 1 + } + else if GetKeySC($ph) ; Normal Keys + Send % part + else ; Auto-type placeholders / text + { + txt := entry.HasKey($ph) ? entry[$ph] : part + if useTCATO && ($ph != "TOTP") + tcato(txt, INI.TCATO.num, INI.TCATO.wait, INI.TCATO.kps) + else if (txt != "{TOTP}") + SendRaw % txt + } + return StrLen(part) +} diff --git a/Lib/autorun.ahk b/Lib/autorun.ahk new file mode 100644 index 0000000..67a3c2d --- /dev/null +++ b/Lib/autorun.ahk @@ -0,0 +1,16 @@ + +autorun() +{ + static state := -1 + , keyDir := "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" + if state = -1 + { + RegRead state, % keyDir, % appTitle + return state := !!state + } + if state ^= 1 + RegWrite REG_SZ, % keyDir, % appTitle, % quote(A_ScriptFullPath) + else + RegDelete % keyDir, % appTitle + Menu Tray, % state ? "Check" : "UnCheck", 6& +} diff --git a/Lib/base32toHex.ahk b/Lib/base32toHex.ahk new file mode 100644 index 0000000..ed87ca6 --- /dev/null +++ b/Lib/base32toHex.ahk @@ -0,0 +1,17 @@ + +base32toHex(base32) +{ + static b32dict := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" + bits := hex := "" ;, base32 := RTrim(base32, "=") + + Loop parse, base32 + val := InStr(b32dict, A_LoopField) - 1 + , bits .= Format("{:05}", dec2bin(val)) + + loop % StrLen(bits) / 4 + start := 1 + A_Index * 4 - 4 + , chunk := SubStr(bits, start, 4) + , hex .= Format("{:x}", bin2dec(chunk)) + + return hex .= Mod(StrLen(hex), 2) ? 0 : "" +} diff --git a/Lib/bin2dec.ahk b/Lib/bin2dec.ahk new file mode 100644 index 0000000..bb2751e --- /dev/null +++ b/Lib/bin2dec.ahk @@ -0,0 +1,9 @@ + +bin2dec(n) +{ + r := 0 + , b := StrLen(n) + loop parse, n + r |= A_LoopField << --b + return r +} diff --git a/Lib/bind.ahk b/Lib/bind.ahk new file mode 100644 index 0000000..574a0df --- /dev/null +++ b/Lib/bind.ahk @@ -0,0 +1,18 @@ + +bind(field, key) +{ + if !INI.SEQUENCES[field] + { + MsgBox % 0x10|0x40000, % appTitle, % "No " quote(field) " sequence defined." + ExitApp 1 + } + fn := Func("findMatch").Bind(field) + Hotkey % key, % fn, UseErrorLevel + if ErrorLevel + { + MsgBox % 0x10|0x40000, % appTitle, % "Invalid hotkey for " quote(field) " field." + ExitApp 1 + } +} + +#Include diff --git a/Lib/bw.ahk b/Lib/bw.ahk new file mode 100644 index 0000000..493c67e --- /dev/null +++ b/Lib/bw.ahk @@ -0,0 +1,33 @@ + +bw(params) +{ + global bwCli + hPipeRead := hPipeWrite := "", cmd := bwCli " " params + , DllCall("Kernel32\CreatePipe", "UInt*",hPipeRead, "UInt*",hPipeWrite, "UInt",0, "UInt",0) + , DllCall("Kernel32\SetHandleInformation", "UInt",hPipeWrite, "UInt",1, "UInt",1) + , VarSetCapacity(STARTUPINFO, 104, 0) + , NumPut(68, STARTUPINFO, 0) + , NumPut(256, STARTUPINFO, 60) + , NumPut(hPipeWrite, STARTUPINFO, 88) + , NumPut(hPipeWrite, STARTUPINFO, 96) + , VarSetCapacity(PROCESS_INFORMATION, 32) + ;TODO: use lpEnvironment + EnvSet BW_SESSION, % SESSION + DllCall("Kernel32\CreateProcess", "UInt",0, "UInt",&cmd, "UInt",0, "UInt",0, "UInt",1, "UInt",0x08000000, "UInt",0, "UInt",0, "UInt",&STARTUPINFO, "UInt",&PROCESS_INFORMATION) + EnvSet BW_SESSION + hProcess := NumGet(PROCESS_INFORMATION, 0) + , hThread := NumGet(PROCESS_INFORMATION, 8) + , DllCall("Kernel32\CloseHandle", "UInt",hPipeWrite) + , VarSetCapacity(buffer, 4096, 0) + , exitCode := out := size := "" + while DllCall("Kernel32\ReadFile", "UInt",hPipeRead, "UInt",&buffer, "UInt",4096, "UInt*",size, "Int",0) + out .= StrGet(&buffer, size, "CP0") + return out + , DllCall("Kernel32\GetExitCodeProcess", "UInt",hProcess, "UInt*",exitCode) + , DllCall("Kernel32\CloseHandle", "UInt",hProcess) + , DllCall("Kernel32\CloseHandle", "UInt",hThread) + , DllCall("Kernel32\CloseHandle", "UInt",hPipeRead) + , VarSetCapacity(STARTUPINFO, 0) + , VarSetCapacity(PROCESS_INFORMATION, 0) + , ErrorLevel := exitCode +} diff --git a/Lib/bwStatus.ahk b/Lib/bwStatus.ahk new file mode 100644 index 0000000..c2e35ec --- /dev/null +++ b/Lib/bwStatus.ahk @@ -0,0 +1,11 @@ + +bwStatus() +{ + bwStatus := bw("status") + bwStatus := JSON.Load(bwStatus) + lastSync := RegExReplace(bwStatus.lastSync, "\D|.{4}$") + ts := epoch(lastSync) + epoch(A_Now) - epoch() + Menu Tray, Tip, % appTitle "`n" epoch_date(ts, "'Sync:' MM/dd/yy h:mm tt") +} + +#Include diff --git a/Lib/checkExe.ahk b/Lib/checkExe.ahk new file mode 100644 index 0000000..4992048 --- /dev/null +++ b/Lib/checkExe.ahk @@ -0,0 +1,16 @@ + +checkExe(path, version := "") +{ + if !attribs := FileExist(path) + return "file not found" + + if InStr(attribs, "D") || SubStr(path, -3) != ".exe" + return "not an executable" + + FileGetVersion exeVersion, % path + if !version + return "couldn't get version" + + if !checkVersion(exeVersion, version) + return "incompatible version" +} diff --git a/Lib/checkVersion.ahk b/Lib/checkVersion.ahk new file mode 100644 index 0000000..d4a55fd --- /dev/null +++ b/Lib/checkVersion.ahk @@ -0,0 +1,14 @@ + +checkVersion(base, required) +{ + base := StrSplit(base, ".") + required := StrSplit(required, ".") + for i,part in required + { + part := Format("{:d}", part) + base[i] := Format("{:d}", base[i]) + if (base[i] < part) + return false + } + return true +} diff --git a/Lib/curl.ahk b/Lib/curl.ahk new file mode 100644 index 0000000..2763927 --- /dev/null +++ b/Lib/curl.ahk @@ -0,0 +1,37 @@ + +; Makes a request like a browser +; when refreshing without cache. +curl(url, file) +{ + static ffVersion := -1 + + if ffVersion = -1 + ffVersion := latestFirefox() + + size := 0 + curl := "curl -fLk" + ; f = Fails for non-200 + ; L = Follows redirects + ; k = Ignore SSL errors + headers := { "DNT": 1 + ; No compression/decompression + , "Accept-Encoding": "identity" + , "Accept-Language": "en-US,en;q=0.9" + ; Don't list aPNG or WebP as accepted formats in favor of regular .ico files + , "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" + , "Cache-Control": "no-cache" + , "Connection": "keep-alive" + , "Pragma": "no-cache" + , "Referer": url + , "Upgrade-Insecure-Requests": 1 + ; User Agent set to latest Firefox obtained dynamically + , "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:" ffVersion ") Gecko/20100101 Firefox/" ffVersion } + for header,val in headers + curl .= " -H " quote(header ": " val) + ; Set & overwrite target + curl .= " -o " quote(file) + RunWait % curl " " quote(url), % A_WorkingDir, Hide UseErrorLevel + if !ErrorLevel + FileGetSize size, % file + return size +} diff --git a/Lib/dec2bin.ahk b/Lib/dec2bin.ahk new file mode 100644 index 0000000..348a92f --- /dev/null +++ b/Lib/dec2bin.ahk @@ -0,0 +1,9 @@ + +dec2bin(n) +{ + r := "" + while n + r := 1 & n r + , n >>= 1 + return r +} diff --git a/Lib/entropy.ahk b/Lib/entropy.ahk new file mode 100644 index 0000000..f60a07a --- /dev/null +++ b/Lib/entropy.ahk @@ -0,0 +1,7 @@ + +entropy(alphabet, length) +{ + ; 0.30103 = log(2) + return Ceil(log(alphabet) / 0.30103 * length) + ; https://en.wikipedia.org/wiki/Password_strength#Random_passwords +} diff --git a/Lib/epoch.ahk b/Lib/epoch.ahk new file mode 100644 index 0000000..05a3112 --- /dev/null +++ b/Lib/epoch.ahk @@ -0,0 +1,15 @@ + +epoch(ts := "") +{ + epoch := (ts ? ts : A_NowUTC) + epoch -= 19700101000000, Secs + return epoch +} + +epoch_date(epoch, format := "dddd MMMM d, yyyy h:mm:ss tt") +{ + ts := 19700101 + ts += epoch, S + FormatTime str, % ts, % format + return str +} diff --git a/Lib/errorReport.ahk b/Lib/errorReport.ahk new file mode 100644 index 0000000..b0b6e83 --- /dev/null +++ b/Lib/errorReport.ahk @@ -0,0 +1,35 @@ + +errorReport(e) +{ + fileName := e.File + SplitPath fileName, fileName + err := INI.Clone() + err.A_Error := { "": "" + , "e.File": fileName + , "e.Line": e.Line + , "e.Message": e.Message + , "e.What": e.What + , "isLocked": isLocked + , "isLogged": isLogged + , "isPortable": !!InStr(A_ScriptFullPath, A_ProgramFiles) + , "session": StrLen(SESSION) + , "status": bwStatus.status + , "sync": bwStatus.lastSync } + err.CREDENTIALS.Delete("user") + if err.TCATO.num + err.TCATO.num := "Redacted" + if err.PIN.use && err.PIN.use != -1 + err.PIN.use := StrLen(err.PIN.use) + if err.PIN.key + RegExMatch(err.PIN.key, "(?<=secret=)\w+", secret) + , err.PIN.key := StrLen(secret) + if err.PIN.hex + err.PIN.hex := "Redacted" + if err.PIN.passwd + err.PIN.Delete("passwd") + FileOpen("debug.txt", 0x1).Write(JSON.Dump(err,, A_Tab)) + MsgBox % 0x4|0x10|0x40000, % appTitle, An error has ocurred and a debug file was generated`, please include it when reporting the bug. Do you want to open it and review it? + IfMsgBox Yes + Run edit debug.txt,, UseErrorLevel + return true +} diff --git a/Lib/faviconFromHtml.ahk b/Lib/faviconFromHtml.ahk new file mode 100644 index 0000000..0cb12c7 --- /dev/null +++ b/Lib/faviconFromHtml.ahk @@ -0,0 +1,82 @@ + +faviconFromHtml(url, file) +{ + link := "" + if !curl(url, A_Temp "\html") + return + + ; Unicode chars + ; Example: icons8.com + FileRead html, % "*P65001 " A_Temp "\html" + FileDelete % A_Temp "\html" + + ; Create a DOM + document := ComObjCreate("HTMLFile") + ; Only tags, otherwise sites like live.com, battle.net + ; and figma.com pop cookie warnings and "Open With" requests. + p := 1, links := "" + while p := RegExMatch(html, "m)]+>", match, p) + links .= match, p += StrLen(match) + document.Write(links) + links := document.getElementsByTagName("link") + + ; Size matrix to pickup default (or smallest) + icons := {} + loop % links.Length + try + { + tag := links[A_Index - 1] + if !(tag.rel ~= "i)icon") + continue + size := 0 + try RegExMatch(tag.sizes, "\d+", size) + icons[size, tag.rel] := tag.href + } + + precedence := ["icon" ; 16, 32, 96 || Android: 192 + , "shortcut icon" ; Old IE + , "alternate-icon" ; Usually png + , "apple-touch-icon" ; 120, 152, 167, 180 || Deprecated: 57, 60, 72, 76, 114, 144 + , "fluid-icon" ; macOS Dock + , "mask-icon" ] ; If svg, is deleted + ; Apple App Icon Sizes (Android defaults to them): + ; https://developer.apple.com/ios/human-interface-guidelines/icons-and-images/app-icon/ + + for size,icon in icons + for i,type in precedence + { + if icons[size].HasKey(type) + { + link := icons[size][type] + break 2 + } + } + + if !link + return + + ; Inline icon + ; Example: canny.io + if InStr(link, ";base64,") + return + + ; Vue.js returns this + ; Example: privalia.com + link := RegExReplace(link, "^about:") + + ; Same protocol, different domain + ; Example: noip.com + if link ~= "^\/\/" + link := SubStr(url, 1, InStr(url, "://")) link + ; Relative + ; Example: assembla.com + else if !(link ~= "^http") + { + path := url + while SubStr(path, 0) != "/" + path := SubStr(path, 1, StrLen(path)-1) + link := path LTrim(link, "/") + } + + return faviconGet(link, file) +} diff --git a/Lib/faviconGet.ahk b/Lib/faviconGet.ahk new file mode 100644 index 0000000..bb1cfa9 --- /dev/null +++ b/Lib/faviconGet.ahk @@ -0,0 +1,15 @@ + +faviconGet(url, file) +{ + ; Download + if !curl(url, file) + return + /* If the downloaded file has an + unrecognized extension, delete + the file, else add extension. + */ + if !ext := iconExtension(file) + FileDelete % file + FileMove % file, % file ext, % true + return !ErrorLevel +} diff --git a/Lib/findMatch.ahk b/Lib/findMatch.ahk new file mode 100644 index 0000000..bae8233 --- /dev/null +++ b/Lib/findMatch.ahk @@ -0,0 +1,92 @@ + +findMatch(mode) +{ + if !isLogged && !toggleLogin() + return + if isLocked && !toggleLock() + return + + ; Early fail if Clipboard unaccessible for TCATO + if INI.TCATO.use && DllCall("User32\GetOpenClipboardWindow", "Ptr") + { + MsgBox % 0x10|0x40000, % appTitle, TCATO cannot access the Clipboard. + return + } + + url := "" + global autoTypeWindow + hWnd := WinExist("A") + autoTypeWindow := hWnd + WinGetClass activeClass + WinGetTitle activeTitle + WinGet exe, ProcessName + + ; Browsers only + if exe contains chrome,msedge,firefox,iexplore,opera + if url := getUrl(hWnd, InStr(exe, "ie")) + splitUrl(url, host, domain) + + atMatches := [] + ; Loop through the JSON + for i,entry in bwFields + { + if (mode = "totp" && !entry.otpauth) + continue + isMatch := false + if url ; by URL + { + switch entry.match + { + ; Never + case 5: continue + ; RegEx + case 4: isMatch := RegExMatch(url, entry.uri) + ; Exact + case 3: isMatch := InStr(url, entry.uri, true) + ; Start + case 2: isMatch := InStr(url, entry.uri) + ; Host + case 1: isMatch := (host = entry.host) + ; Base Domain + default: ; case 0 and NULL + isMatch := (domain = entry.domain) + } + } + ; by .exe + else if SubStr(entry.uri, -3) = ".exe" + isMatch := (exe = entry.uri) + ; by class + else if RegExMatch(entry.uri, "class=(.+)", $) + isMatch := ($1 = activeClass) + ; by window title, exact match + else if RegExMatch(entry.uri, "title=(.+)", $) + isMatch := ($1 == activeTitle) + ; by window title, partial match + else if !entry.schema + isMatch := InStr(activeTitle, entry.uri) + + if isMatch + { + ; Add a typing sequence + if (entry.HasKey("field") && mode = "default") + entry.sequence := entry.field + else + entry.sequence := INI.SEQUENCES[mode] + atMatches.Push(entry) + } + } + + ; End if no matches + if !total := atMatches.Count() + { + MsgBox % 0x10|0x40000, % appTitle, No auto-type match found. + return + } + + ; Multiple matches + if total = 1 + autoType(atMatches[1], mode) + else + selectMatch(atMatches, mode) + +} diff --git a/Lib/formatOtp.ahk b/Lib/formatOtp.ahk new file mode 100644 index 0000000..8725d91 --- /dev/null +++ b/Lib/formatOtp.ahk @@ -0,0 +1,6 @@ + +formatOtp(otp) +{ + mid := StrLen(otp) // 2 + return SubStr(otp, 1, mid) " " SubStr(otp, ++mid) +} diff --git a/Lib/generator.ahk b/Lib/generator.ahk new file mode 100644 index 0000000..1efa186 --- /dev/null +++ b/Lib/generator.ahk @@ -0,0 +1,106 @@ + +generator() +{ + Gui Generator:New, +AlwaysOnTop +LabelGenerator +LastFound +ToolWindow + Gui Font, s9 q5, Consolas + Gui Add, CheckBox, % (INI.GENERATOR.lower ? "Checked" : "") " x10 y10" , Lower + Gui Add, CheckBox, % (INI.GENERATOR.upper ? "Checked" : "") " xp+70 yp", Upper + Gui Add, CheckBox, % (INI.GENERATOR.digits ? "Checked" : "") " xp+70 yp", Digits + Gui Add, CheckBox, % (INI.GENERATOR.symbols ? "Checked" : "") " xp+70 yp", Symbols + Gui Add, Text, x10 yp+30, Length: + Gui Add, Edit, w50 xp+70 yp-5 + Gui Add, UpDown, Range1-999, % INI.GENERATOR.length + Gui Add, Text, xp+70 yp+6, Exclude: + Gui Add, Edit, gGenerator_filter r1 w70 xp+70 yp-5, % INI.GENERATOR.exclude + Gui Add, Text, x10 yp+35, Password: + Gui Add, Edit, w210 xp+70 yp-5 + Gui Add, Text, x10 yp+35, % "Entropy: 0 bits " + Gui Add, Button, x170 yp-6, Copy + Gui Add, Button, Default x222 yp, Generate + Gui Show,, Secure Password Generator + if INI.TCATO.use + Random ,, % epoch() + getControls() + OnMessage(0x0101, "generator_monitor") ; WM_KEYUP + OnMessage(0x0202, "generator_monitor") ; WM_LBUTTONUP + OnMessage(0x020A, "generator_monitor") ; WM_MOUSEWHEEL + WinWaitClose +} + +generator_filter(ctrlHwnd) +{ + GuiControlGet value,, % ctrlHwnd + filtered := "" + loop parse, value + if !InStr(filtered, A_LoopField) + filtered .= A_LoopField + GuiControl ,, % ctrlHwnd, % filtered + Send {End} +} + +generator_monitor(wParam, lParam, msg, hWnd) +{ + global guiControls + controlID := guiControls[hWnd] + if msg = 0x20A ; Scroll + { + if update := (controlID = "Edit1") + { + GuiControlGet value,, Edit1 + value += wParam = 7864320 ? 1 : -1 + INI.GENERATOR.length := value + } + } + else ; Click/Type + { + lParam := lParam >> 16 & 255 + if lParam = 15 ; Tab + return + else if lParam in 72,80 ; Up,Down + { + GuiControlGet focused, Focus + if (focused != "Edit1") + return + } + update := true + ; Checkboxes + if controlID ~= "Button[1-4]" + { + GuiControlGet isChecked,, % controlID + INI.GENERATOR[A_GuiControl] := !isChecked + } + ; Length + else if controlID ~= "Edit1|updown" + { + GuiControlGet value,, Edit1 + INI.GENERATOR.length := value + } + ; Exclude + else if (controlID = "Edit2") + { + GuiControlGet value,, Edit2 + INI.GENERATOR.exclude := value + } + else if (A_GuiControl = "Copy") + { + GuiControlGet Clipboard,, Edit3 + return + } + else if (A_GuiControl != "Generate") + update := false + } + if update + { + GuiControl Text, Edit3, % passwdGen(INI.GENERATOR, entropy) + GuiControl Text, Static4, % "Entropy: " entropy " bits" + } +} + + +GeneratorClose: +GeneratorEscape: + Gui Destroy + for key,val in INI.GENERATOR + IniWrite % " " val, % settings, GENERATOR, % key + ; ↑↑↑ https://i.imgur.com/i2CZlQR.jpg +return diff --git a/Lib/getControls.ahk b/Lib/getControls.ahk new file mode 100644 index 0000000..db3d214 --- /dev/null +++ b/Lib/getControls.ahk @@ -0,0 +1,11 @@ + +getControls() +{ + global guiControls := [] + WinGet types, ControlList + types := StrSplit(types, "`n") + WinGet hWndList, ControlListHwnd + loop parse, hWndList, `n + guiControls[A_LoopField] := types[A_Index] + return guiControls +} diff --git a/Lib/getData.ahk b/Lib/getData.ahk new file mode 100644 index 0000000..aa412ce --- /dev/null +++ b/Lib/getData.ahk @@ -0,0 +1,9 @@ + +getData() +{ + Menu Tray, Icon, shell32.dll, 239 + bwFields := bw("list items") + bwFields := JSON.Load(bwFields) + bwFields := parseItems(bwFields) + Menu Tray, Icon, % A_IsCompiled ? A_ScriptFullPath : A_ScriptDir "\assets\bw-at.ico" +} diff --git a/Lib/getFavicons.ahk b/Lib/getFavicons.ahk new file mode 100644 index 0000000..e1bd1fb --- /dev/null +++ b/Lib/getFavicons.ahk @@ -0,0 +1,53 @@ + +getFavicons() +{ + ; First download + if !FileExist("icons") + FileCreateDir icons + else + { + ; Once per week + FileGetTime mTime, icons + if epoch() < epoch(mTime) + 604800 + return + ; Delete generics + loop files, icons\* + if A_LoopFileSize = 344 + FileDelete % A_LoopFileFullPath + } + + for i,entry in bwFields + { + if !InStr(entry.schema, "http") + continue + + ; Already Downloaded + file := "icons\" entry.host "." + if FileExist(file "*") + continue + + ; On the host root: 500px.com + if faviconGet(entry.schema entry.host "/favicon.ico", file) + continue + + ; Base domain: community.bitwarden.com -> bitwarden.com + if (entry.host != entry.domain) + && faviconGet(entry.schema entry.domain "/favicon.ico", file) + continue + + ; Base domain with www: teamviewer.com -> www.teamviewer.com + if (entry.host != "www." entry.domain) + && faviconGet(entry.schema "www." entry.domain "/favicon.ico", file) + continue + + ; Favicon in HTML: zoom.us -> st1.zoom.us/zoom.ico + if faviconFromHtml(entry.uri, file) + continue + + ; Bitwarden as failover: battle.net + faviconGet("https://icons.bitwarden.net/" entry.host "/icon.png", file) + } + + ; ListView unsupported + FileDelete icons\*.webp +} diff --git a/Lib/getPassword.ahk b/Lib/getPassword.ahk new file mode 100644 index 0000000..04f949e --- /dev/null +++ b/Lib/getPassword.ahk @@ -0,0 +1,9 @@ + +getPassword() +{ + ; Ask for password + InputBox bwPass, % appTitle, Master Password:, HIDE, 190, 125,,, Locale + if !bwPass + return + return quote(bwPass) ; Enclose in quotes to avoid escaping +} diff --git a/Lib/getUrl.ahk b/Lib/getUrl.ahk new file mode 100644 index 0000000..5053c9f --- /dev/null +++ b/Lib/getUrl.ahk @@ -0,0 +1,32 @@ + +; Tested with the latest versions as of May, 2021. +; Chrome, Edge, Firefox and Opera (market share above 1%) +; https://en.wikipedia.org/wiki/Usage_share_of_web_browsers#Summary_tables + +getUrl(hWnd, force := false) +{ + static addressBar := [] + if !addressBar[hWnd] || force + addressBar[hWnd] := getUrl_Bar(Acc_ObjectFromWindow(hWnd)) + try + url := addressBar[hWnd].accValue(0) + catch e + { + url := "" + if InStr(e.Message, "800401FD") + { + addressBar.Delete(hWnd) + return %A_ThisFunc%(hWnd) + } + } + return url +} + +getUrl_Bar(accObj) +{ + if accObj.accValue(0) && InStr(accObj.accName(0), "Address") + return accObj + for i,accChild in Acc_Children(accObj) + if IsObject(accObj := %A_ThisFunc%(accChild)) + return accObj +} diff --git a/Lib/hex2dec.ahk b/Lib/hex2dec.ahk new file mode 100644 index 0000000..22a9bd0 --- /dev/null +++ b/Lib/hex2dec.ahk @@ -0,0 +1,5 @@ + +hex2dec(h) +{ + return Format("{:d}", "0x" LTrim(h, "0x")) +} diff --git a/Lib/iconExtension.ahk b/Lib/iconExtension.ahk new file mode 100644 index 0000000..627c045 --- /dev/null +++ b/Lib/iconExtension.ahk @@ -0,0 +1,21 @@ + +iconExtension(file) +{ + ; With just a byte jpg clashes with + ; text encoded in UTF-16 with BOM. + VarSetCapacity(bytes, 2, 0) + ; Read just 2 bytes from the header + ; as BMP changes from the 3rd byte. + FileOpen(file, 0x0).RawRead(bytes, 2) + if ErrorLevel + return + header := NumGet(bytes, 0, "UChar") + . "," NumGet(bytes, 1, "UChar") + return { "" : "" + , "66,77" : "bmp" + , "71,73" : "gif" + , "0,0" : "ico" + , "255,216" : "jpg" + , "137,80" : "png" + , "82,73" : "webp" }[header] +} diff --git a/Lib/init.ahk b/Lib/init.ahk new file mode 100644 index 0000000..8e278af --- /dev/null +++ b/Lib/init.ahk @@ -0,0 +1,61 @@ + +init() +{ + ; Active vault + EnvSet BW_RAW, % "true" + EnvSet BW_NOINTERACTION, % "true" + EnvSet BITWARDENCLI_APPDATA_DIR, % A_WorkingDir + isLocked := isLogged := FileOpen("data.json", 0x2).Length > 512 + + if isLocked + { + bwStatus() + passwd := toggleLock(false) + } + else + { + passwd := toggleLogin(false) + SetTimer bwStatus, -1 ; Async + } + + if isLocked + ExitApp 1 + + ; Decrypt data + getData() + + ; Acknowledge + Menu Tray, Icon, % A_IsCompiled ? A_ScriptFullPath : A_ScriptDir "\assets\bw-at.ico" + TrayTip % appTitle, Auto-Type Ready, 10, 0x20 + + ; Setup PIN + if INI.PIN.use = -1 && !INI.PIN.hex && pin := pinSetup() + { + pin .= bwStatus.userId, iv := bwStatus.userEmail + INI.PIN.hex := Crypt.Encrypt.String("AES", "CBC", passwd, pin, iv,, "HEXRAW") + IniWrite % " " INI.PIN.hex, % settings, PIN, hex + } + else if INI.PIN.use && INI.PIN.use != -1 + INI.PIN.passwd := passwd + + ; TCATO + if INI.TCATO.use + RegExMatch(bwStatus.userId, "\w+", num) + , INI.TCATO.num := "0x" num + + ; Favicons + if !INI.GENERAL.favicons + return + + /* UrlDownloadToFile is way too primitive thus file + download rely on cURL, shipped with W10 from builds + 1803 onwards (April 2018), check for availability. + */ + if !FileExist("C:\Windows\System32\curl.exe") + { + MsgBox % 0x10|0x40000, % appTitle, cURL is not available. + IniWrite % "", % settings, GENERAL, favicons + return + } + getFavicons() +} diff --git a/Lib/latestFirefox.ahk b/Lib/latestFirefox.ahk new file mode 100644 index 0000000..aa7f540 --- /dev/null +++ b/Lib/latestFirefox.ahk @@ -0,0 +1,10 @@ + +latestFirefox() +{ + UrlDownloadToFile https://formulae.brew.sh/api/cask/firefox.json, % A_Temp "\json" + FileRead json, % A_Temp "\json" + FileDelete, % A_Temp "\json" + if !RegExMatch(json, "(?<=version.:.)[\d\.]+", version) + version := "88.0.1" ; May, 2021 + return version +} diff --git a/Lib/loadIni.ahk b/Lib/loadIni.ahk new file mode 100644 index 0000000..710d794 --- /dev/null +++ b/Lib/loadIni.ahk @@ -0,0 +1,15 @@ + +loadIni(file) +{ + obj := {} + IniRead sections, % file + loop parse, sections, `n, `r + { + sect := A_LoopField + IniRead conts, % file, % sect + loop parse, conts, `n, `r + parts := StrSplit(A_LoopField, "=", "`t ", 2) + , obj[sect, parts[1]] := parts[2] + } + return obj +} diff --git a/Lib/menu.ahk b/Lib/menu.ahk new file mode 100644 index 0000000..9a0bcbc --- /dev/null +++ b/Lib/menu.ahk @@ -0,0 +1,32 @@ + +menu() +{ + ; Tray menu + Menu Tray, Icon + Menu Tray, Icon, shell32.dll, 48 + Menu Tray, NoStandard + Menu Tray, Tip, % appTitle " (Loading...)" + Menu Tray, Add, &Sync , sync ; 1 + Menu Tray, Add, &Lock , toggleLock ; 2 + Menu Tray, Add, Log&in, toggleLogin ; 3 + Menu Tray, Add + Menu Tray, Add, &TCATO , tcato_menu ; 5 + Menu Tray, Add, &Autorun , autorun ; 6 + Menu Tray, Add, &Settings , settings + Menu Tray, Add, &Generator , generator + Menu Tray, Add, &Open Vault, openVault + Menu Tray, Add + Menu Tray, Add, &Exit, menuExit + Menu Tray, Disable, 1& + Menu Tray, Disable, 2& + if INI.TCATO.use + Menu Tray, Check, 5& + if autorun() + Menu Tray, Check, 6& +} + +#Include +#Include +#Include +#Include +#Include diff --git a/Lib/menuExit.ahk b/Lib/menuExit.ahk new file mode 100644 index 0000000..75f31a5 --- /dev/null +++ b/Lib/menuExit.ahk @@ -0,0 +1,5 @@ + +menuExit() +{ + ExitApp +} diff --git a/Lib/openVault.ahk b/Lib/openVault.ahk new file mode 100644 index 0000000..191f130 --- /dev/null +++ b/Lib/openVault.ahk @@ -0,0 +1,5 @@ + +openVault() +{ + Run https://vault.bitwarden.com/,, UseErrorLevel +} diff --git a/Lib/parseItems.ahk b/Lib/parseItems.ahk new file mode 100644 index 0000000..85cbccb --- /dev/null +++ b/Lib/parseItems.ahk @@ -0,0 +1,55 @@ + +parseItems(items) +{ + out := [] + for i,entry in items + { + ; Logins only + if entry.type != 1 + continue + + ; 2FA unlock + if (INI.PIN.use = entry.name) + { + if totp(entry.login.totp) ~= "\d{6}" + INI.PIN.key := entry.login.totp + else + INI.PIN.use := false + } + + ; Item definition + base := { "name": entry.name + , "otpauth": entry.login.totp + , "username": entry.login.username + , "password": entry.login.password } + + ; Custom auto-type + for j,field in entry.fields + switch field.name + { + case "TCATO": base.TCATO := field.value + case INI.SEQUENCES["field"]: base.field := field.value + } + + ; Parse each URI + for j,uri in entry.login.uris + { + ; Avoid references + item := base.Clone() + item.match := uri.match + splitUrl(uri.uri, host, domain, schema, resource) + item.host := host ; .exe name as host for icon + if InStr(schema, "http") + item.schema := schema + , item.domain := domain + , item.uri := uri.match = 4 ? uri.uri : host resource ; match 4 is a RegEx, don't modify + else if InStr(schema, "app") + item.schema := "app://" + , item.uri := host resource + else + item.uri := uri.uri + out.Push(item) + } + } + return out +} diff --git a/Lib/passwdGen.ahk b/Lib/passwdGen.ahk new file mode 100644 index 0000000..8fe564b --- /dev/null +++ b/Lib/passwdGen.ahk @@ -0,0 +1,25 @@ + +passwdGen(with, ByRef entropy) +{ + out := from := "" + if with.lower + from .= "abcdefghijklmnopqrstuvwxyz" + if with.upper + from .= "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + if with.digits + from .= "0123456789" + if with.symbols + from .= "!""#$%&'()*+,-./:;<=>?@[\]^_``{|}~" + StringCaseSense On + loop parse, % with.exclude + from := StrReplace(from, A_LoopField) + StringCaseSense Off + dict := StrLen(from) + from := StrSplit(from) + loop % with.length + { + Random rnd, 1, % dict + out .= from[rnd] + } + return out, entropy := entropy(dict, with.length) +} diff --git a/Lib/pin.ahk b/Lib/pin.ahk new file mode 100644 index 0000000..a96e41c --- /dev/null +++ b/Lib/pin.ahk @@ -0,0 +1,52 @@ + +pin(title) +{ + INI.PIN.pin := "" + Gui New, +AlwaysOnTop +LastFound +ToolWindow + Gui Font, s15 q5 w1000, Consolas + loop 6 + Gui Add, Edit, % "Center Limit1 Number w30 y10 x" A_Index * 40 - 20 + Gui Show,, % title + getControls() + OnMessage(0x101, "pin_type") ; WM_KEYUP + WinWaitClose + return INI.PIN.pin +} + +pin_type(wParam, lParam, msg, hWnd) +{ + static pin := [] + global guiControls + + key := GetKeyName(Format("sc{:X}", lParam >> 16 & 0xFF)) + if key ~= "Shift|Tab" || GetKeyState("Shift", "P") + return + if key ~= "^[^0-9]" ; Digits and Numpad-digits only + && (key ~= "^[^Num]" && GetKeyState("NumLock", "T")) + return + + value := "" + controlID := guiControls[hWnd] + GuiControlGet value,, % controlID + if StrLen(value) + { + pin[controlID] := value + if pin.Count() = 6 + { + for i,num in pin + INI.PIN.pin .= num + pin := [] + Gui Destroy + } + else + Send {Tab} + } + else + pin.Delete(controlID) +} + + +GuiClose: +GuiEscape: + Gui Destroy +return diff --git a/Lib/pinSetup.ahk b/Lib/pinSetup.ahk new file mode 100644 index 0000000..416784e --- /dev/null +++ b/Lib/pinSetup.ahk @@ -0,0 +1,13 @@ + +pinSetup() +{ + pin1 := pin("PIN Setup") + if StrLen(pin1) != 6 + return + pin2 := pin("PIN Repeat") + if StrLen(pin2) != 6 + return + if (pin1 = pin2) + return pin2 + return %A_ThisFunc%() +} diff --git a/Lib/quote.ahk b/Lib/quote.ahk new file mode 100644 index 0000000..74df04e --- /dev/null +++ b/Lib/quote.ahk @@ -0,0 +1,5 @@ + +quote(str) +{ + return """" str """" +} diff --git a/Lib/retry2yes.ahk b/Lib/retry2yes.ahk new file mode 100644 index 0000000..674fcb1 --- /dev/null +++ b/Lib/retry2yes.ahk @@ -0,0 +1,8 @@ + +retry2yes() +{ + if !WinActive("ahk_pid " DllCall("Kernel32\GetCurrentProcessId")) + return + ControlSetText Button1, &Yes + SetTimer retry2yes, Delete +} diff --git a/Lib/selectMatch.ahk b/Lib/selectMatch.ahk new file mode 100644 index 0000000..b62fc96 --- /dev/null +++ b/Lib/selectMatch.ahk @@ -0,0 +1,77 @@ + +selectMatch(matches, mode) +{ + global matchId := 0 + , matchObj := matches + , matchMode := mode + labels := { "default": "Default" + , "username": "Username-only" + , "password": "Password-only" + , "totp": "TOTP" } + total := matches.Count() + + Gui New, +AlwaysOnTop +LabelMatchGui +LastFound +ToolWindow + Gui Font, s10 + Gui Add, ListView, AltSubmit Grid gSelectMatch_Row, % "|#|Entry|User" (mode = "totp" ? "" : "|TOTP") + Gui Add, Button, Default gSelectMatch_Use x-50 y-50 + + LV_SetImageList(iconList := IL_Create(total)) + for i,match in matches + { + if SubStr(match.host, -3) = ".exe" + { + WinGet exe, ProcessPath, % "ahk_exe" match.host + exe ? IL_Add(iconList, exe) + : IL_Add(iconList, "shell32.dll", 3) + } + else + { + img := "" + loop files, % "icons\" match.host ".*" + img := A_LoopFileFullPath + loop files, % "icons\" match.domain ".*" + img := A_LoopFileFullPath + img ? IL_Add(iconList, img, 0xFFFFFF, 1) + : IL_Add(iconList, "imageres.dll", 300) + } + if (mode = "totp") + LV_Add("Icon" i,, i, match.name, match.username) + else + LV_Add("Icon" i,, i, match.name, match.username, match.otpauth ? "✔" : "✘") + } + + ; Auto-size + LV_ModifyCol() + LV_ModifyCol(2, "Center") + if (mode != "totp") + LV_ModifyCol(5, "AutoHdr Center") + cols := LV_GetCount("Column") + listViewWidth := cols * cols + Loop % cols + { + SendMessage 0x101D, % A_Index - 1, 0x0, SysListView321 ; LVM_GETCOLUMNWIDTH + listViewWidth += ErrorLevel + } + GuiControl Move, SysListView321, % "w" listViewWidth + Gui Show, AutoSize, % " Select Entry (" labels[mode] " sequence)" +} + +selectMatch_Row(ctrlHwnd, guiEvent, eventInfo, errLevel := "") +{ + global matchId := EventInfo + if (GuiEvent = "DoubleClick") + selectMatch_Use() +} + +selectMatch_Use() +{ + global + Gosub MatchGuiClose + autoType(matchObj[matchId], matchMode) +} + +MatchGuiClose: +MatchGuiEscape: + Gui %A_Gui%:Destroy + WinActivate % "ahk_id" autoTypeWindow +return diff --git a/Lib/settings.ahk b/Lib/settings.ahk new file mode 100644 index 0000000..15f9565 --- /dev/null +++ b/Lib/settings.ahk @@ -0,0 +1,26 @@ + +settings() +{ + SetTimer fileUpdate, 1000 + SplitPath settings, fileName + if !WinExist(fileName) + Run % "edit " settings + WinWait % fileName + WinWaitClose % fileName + SetTimer fileUpdate, Delete +} + +fileUpdate() +{ + static last := 0 + FileGetTime mTime, % settings + if !last + last := mTime + if (last != mTime) + { + MsgBox % 0x4|0x20|0x40000, % appTitle, Application needs to be reloaded for the changes to take effect. Reload now? + IfMsgBox Yes + Reload + last := mTime + } +} diff --git a/Lib/signExe.ahk b/Lib/signExe.ahk new file mode 100644 index 0000000..3418229 --- /dev/null +++ b/Lib/signExe.ahk @@ -0,0 +1,132 @@ + +; Reformat of +; https://github.com/Lexikos/AutoHotkey-Release/blob/master/installer/source/Lib/EnableUIAccess.ahk + +signExe(filename, certName, uia := false) +{ + if !hStore := DllCall("Crypt32\CertOpenStore", "Ptr",10, "UInt",0, "Ptr",0, "UInt",0x20000, "WStr","Root", "Ptr") + throw + p := DllCall("Crypt32\CertFindCertificateInStore", "Ptr",hStore, "UInt",0x10001, "UInt",0, "UInt",0x80007, "WStr",certName, "Ptr",0, "Ptr") + cert := p ? new CertContext(p) : signExe_CreateCert(certName, hStore) + if uia + signExe_SetManifest(filename) + signExe_SignFile(filename, cert, certName) +} + +signExe_CreateCert(CertName, hStore) +{ + if !DllCall("Advapi32\CryptAcquireContext", "Ptr*",hProv, "Str",CertName, "Ptr",0, "UInt",1, "UInt",0) + { + if !DllCall("Advapi32\CryptAcquireContext", "Ptr*",hProv, "Str",CertName, "Ptr",0, "UInt",1, "UInt",8) + throw + prov := new CryptContext(hProv) + if !DllCall("Advapi32\CryptGenKey", "Ptr",hProv, "UInt",2, "UInt",0x4000001, "Ptr*",hKey) + throw + (new CryptKey(hKey)) + } + loop 2 + { + if A_Index = 1 + pbName := cbName := 0 + else + VarSetCapacity(bName, cbName), pbName := &bName + if !DllCall("Crypt32\CertStrToName", "UInt",1, "Str","CN=" CertName, "UInt",3, "Ptr",0, "Ptr",pbName, "UInt*", cbName, "Ptr",0) + throw + } + VarSetCapacity(cnb, 2*A_PtrSize) + NumPut(pbName, NumPut(cbName, cnb)) + VarSetCapacity(endTime, 16) + DllCall("Kernel32\GetSystemTime", "Ptr",&endTime) + NumPut(NumGet(endTime, "UShort") + 10, endTime, "UShort") + if !hCert := DllCall("Crypt32\CertCreateSelfSignCertificate", "Ptr",hProv, "Ptr",&cnb, "UInt",0, "Ptr",0, "Ptr",0, "Ptr",0, "Ptr",&endTime, "Ptr",0, "Ptr") + throw + cert := new CertContext(hCert) + if !DllCall("Crypt32\CertAddCertificateContextToStore", "Ptr",hStore, "Ptr",hCert, "UInt",1, "Ptr",0) + throw + return cert +} + +signExe_DeleteCert(CertName) +{ + DllCall("Advapi32\CryptAcquireContext", "Ptr*",undefined, "Str",CertName, "Ptr",0, "UInt",1, "UInt",16) + if !hStore := DllCall("Crypt32\CertOpenStore", "Ptr",10, "UInt",0, "Ptr",0, "UInt",0x20000, "WStr","Root", "Ptr") + throw + if !p := DllCall("Crypt32\CertFindCertificateInStore", "Ptr",hStore, "UInt",0x10001, "UInt",0, "UInt",0x80007, "WStr",CertName, "Ptr",0, "Ptr") + return 0 + if !DllCall("Crypt32\CertDeleteCertificateFromStore", "Ptr",p) + throw + return 1 +} + +signExe_SetManifest(file) +{ + xml := ComObjCreate("Msxml2.DOMDocument") + xml.async := false + xml.setProperty("SelectionLanguage", "XPath") + xml.setProperty("SelectionNamespaces", "xmlns:v1='urn:schemas-microsoft-com:asm.v1' xmlns:v3='urn:schemas-microsoft-com:asm.v3'") + if !xml.load("res://" file "/#24/#1") + throw + if !node := xml.selectSingleNode("/v1:assembly/v3:trustInfo/v3:security/v3:requestedPrivileges/v3:requestedExecutionLevel") + throw + node.setAttribute("uiAccess", "true") + xml := RTrim(xml.xml, "`r`n") + VarSetCapacity(data, data_size := StrPut(xml, "UTF-8") - 1) + StrPut(xml, &data, "UTF-8") + if !hupd := DllCall("Kernel32\BeginUpdateResource", "Str",file, "Int",false) + throw + r := DllCall("Kernel32\UpdateResource", "Ptr",hupd, "Ptr",24, "Ptr",1, "UShort",1033, "Ptr",&data, "UInt",data_size) + if !DllCall("Kernel32\EndUpdateResource", "Ptr",hupd, "Int",!r) && r + throw +} + +signExe_SignFile(File, CertCtx, Name) +{ + VarSetCapacity(dwIndex, 4, 0), cert_Ptr := IsObject(CertCtx) ? CertCtx.p : CertCtx + VarSetCapacity(wfile, 2 * StrPut(File, "UTF-16")), StrPut(File, &wfile, "UTF-16") + VarSetCapacity(wname, 2 * StrPut(Name, "UTF-16")), StrPut(Name, &wname, "UTF-16") + signExe_Struct(file_info, "Ptr",A_PtrSize * 3, "Ptr",&wfile) + signExe_Struct(subject_info, "Ptr",A_PtrSize * 4, "Ptr",&dwIndex, "Ptr",1, "Ptr",&file_info) + signExe_Struct(cert_store_info, "Ptr",A_PtrSize * 4, "Ptr",cert_Ptr, "Ptr",2) + signExe_Struct(cert_info, "UInt",8 + A_PtrSize * 2, "UInt",2, "Ptr",&cert_store_info) + signExe_Struct(authcode_attr, "UInt",8 + A_PtrSize * 3, "Int",false, "Ptr",true, "Ptr",&wname) + signExe_Struct(sig_info, "UInt",8 + A_PtrSize * 4, "UInt",0x8004, "Ptr",1, "Ptr",&authcode_attr) + hr := DllCall("MSSign32\SignerSign", "Ptr",&subject_info, "Ptr",&cert_info, "Ptr",&sig_info, "Ptr",0, "Ptr",0, "Ptr",0, "Ptr",0, "UInt") + if hr != 0 + throw hr +} + +signExe_Struct(ByRef struct, arg*) +{ + VarSetCapacity(struct, arg[2], 0), p := &struct + loop % arg.Length() // 2 + p := NumPut(arg[2], p+0, arg[1]), arg.RemoveAt(1, 2) + return &struct +} + +class CryptContext +{ + __New(p) + { + this.p := p + } + __Delete() + { + DllCall("Advapi32\CryPtreleaseContext", "Ptr",this.p, "UInt",0) + } +} + +class CertContext extends CryptContext +{ + __Delete() + { + DllCall("Crypt32\CertFreeCertificateContext", "Ptr",this.p) + } +} + +class CryptKey extends CryptContext +{ + __Delete() + { + DllCall("Advapi32\CryptDestroyKey", "Ptr",this.p) + } +} diff --git a/Lib/splItUrl.ahk b/Lib/splItUrl.ahk new file mode 100644 index 0000000..2c47f8b --- /dev/null +++ b/Lib/splItUrl.ahk @@ -0,0 +1,13 @@ + +splitUrl(url, ByRef host, ByRef domain, ByRef schema := "", ByRef resource := "") +{ + RegExMatch(url, "S)(?.+:\/\/)?(?[^\/]+)(?.*)", $) + schema := $Schema, host := $Host, resource := $Resource + + ; TLDs now are just stupid (because IDN) + ; http://data.iana.org/TLD/tlds-alpha-by-domain.txt + ; https://en.wikipedia.org/wiki/Internationalized_domain_name + ; No way around 3 letter domains (eg, "foo.git.io" should be "git.io", interpreted like example.com.mx) + RegExMatch($Host, "S)(?[^\.\/]+\.(?:[^\.\/]{2,24}|[^\.\/]{2,3}\.[^\.\/]{2}))(?:\/|$)", $) + domain := $Domain +} diff --git a/Lib/sync.ahk b/Lib/sync.ahk new file mode 100644 index 0000000..6b3a23e --- /dev/null +++ b/Lib/sync.ahk @@ -0,0 +1,19 @@ + +sync(showTip := false) +{ + if !isLogged || isLocked + return + Menu Tray, Icon, shell32.dll, 239 + bw("sync") + SetTimer bwStatus, -1 + getData() + if showTip + TrayTip % appTitle, Sync complete, 10, 0x20 +} + +sync_auto(mins) +{ + if !period := 1000 * 60 * mins + return + SetTimer sync, % period +} diff --git a/Lib/tcato.ahk b/Lib/tcato.ahk new file mode 100644 index 0000000..46f3a00 --- /dev/null +++ b/Lib/tcato.ahk @@ -0,0 +1,40 @@ + +; Two-Channel Auto-Type Obfuscation +; https://keepass.info/help/v2/autotype_obfuscation.html + +tcato(str, seed := 0, wait := 500, kps := 10) +{ + sendPart := [] + clipPart := "" + Clipboard := "" + Random ,, % seed + loop parse, str + { + Random rnd, 0, 1 + if rnd + clipPart .= A_LoopField + else + sendPart[A_Index] := A_LoopField + } + Clipboard := clipPart + ClipWait + Send ^v + Sleep % wait + Clipboard := "" + Send % "{Left " StrLen(clipPart) "}" + SetKeyDelay % 1000 / kps + loop parse, str + { + chr := sendPart[A_Index] + Send % StrLen(chr) ? "{Raw}" chr : "{Right}" + } +} + +tcato_menu() +{ + if INI.TCATO.use := !INI.TCATO.use + IniWrite % " " 1, % settings, TCATO, use + else + IniWrite % "", % settings, TCATO, use + Menu Tray, % INI.TCATO.use ? "Check" : "UnCheck", 5& +} diff --git a/Lib/toggleLock.ahk b/Lib/toggleLock.ahk new file mode 100644 index 0000000..2b19910 --- /dev/null +++ b/Lib/toggleLock.ahk @@ -0,0 +1,54 @@ + +toggleLock(showTip := false) +{ + if isLocked + { + ; Use PIN + tries := 0 + passwd := "" + while !passwd + && tries++ < 3 + && INI.PIN.use + && (INI.PIN.hex || INI.PIN.key) + && StrLen(pin := pin("Vault Unlock")) + { + if INI.PIN.hex + { + hex := INI.PIN.hex, pin .= bwStatus.userId, iv := bwStatus.userEmail + try passwd := Crypt.Decrypt.String("AES", "CBC", hex, pin, iv,, "HEXRAW") + } + else if totp(INI.PIN.key) = pin + passwd := INI.PIN.passwd + } + if !passwd && !passwd := getPassword() + return + + out := bw("unlock " passwd) + if ErrorLevel + { + MsgBox % 0x10|0x40000, % appTitle, % out + Exit + } + SESSION := out + + ; Update Menu + Menu Tray, Enable, 1& + Menu Tray, Enable, 2& + Menu Tray, Rename, 2&, &Lock + Menu Tray, Rename, 3&, Log&out + Menu Tray, Icon, % A_IsCompiled ? A_ScriptFullPath : A_ScriptDir "\assets\bw-at.ico" + isLocked := false + if showTip + TrayTip % appTitle, Vault unlocked, 10, 0x20 + return passwd + } + else + { + Menu Tray, Disable, 1& + Menu Tray, Rename , 2&, Un&Lock + if showTip + TrayTip % appTitle, Vault locked, 10, 0x20 + Menu Tray, Icon, shell32.dll, 48 + isLocked := true + } +} diff --git a/Lib/toggleLogin.ahk b/Lib/toggleLogin.ahk new file mode 100644 index 0000000..6352240 --- /dev/null +++ b/Lib/toggleLogin.ahk @@ -0,0 +1,61 @@ + +toggleLogin(showTip := true) +{ + FileOpen("data.json", 0x1) + if isLogged + { + isLogged := false + TrayTip % appTitle, Logged out, 10, 0x20 + ; Update Menu + Menu Tray, Disable, 1& + Menu Tray, Disable, 2& + Menu Tray, Rename , 3&, Log&in + Menu Tray, Icon, shell32.dll, 48 + } + else + { + if !passwd := getPassword() + return + cmd := "login " INI.CREDENTIALS.user " " passwd + bw2FA := SubStr(INI.CREDENTIALS.2fa, 1, 1) + if bw2FA in A,E,Y + { + if (bw2FA = "E") ; Trigger email + { + global bwCli + Run % bwCli " " cmd " --method 1",, Hide UseErrorLevel + } + if (bw2FA = "Y") ; Yubikey methods + InputBox code, % appTitle, YubiKey Code,, 190, 125,,, Locale + else + { + code := pin("2FA Code") + if StrLen(code) != 6 + return + } + methods := { "A":0, "E":1, "Y":3 } + cmd .= " --method " methods[bw2FA] " --code " code + } + + ; Store session information + out := bw(cmd) + if ErrorLevel + { + MsgBox % 0x10|0x40000, % appTitle, % out + Exit + } + SESSION := out + + isLogged := true + isLocked := false + ; Update Menu + Menu Tray, Enable, 1& + Menu Tray, Enable, 2& + Menu Tray, Rename, 2&, &Lock + Menu Tray, Rename, 3&, Log&out + Menu Tray, Icon, % A_IsCompiled ? A_ScriptFullPath : A_ScriptDir "\assets\bw-at.ico" + if showTip + TrayTip % appTitle, Logged In, 10, 0x20 + return passwd + } +} diff --git a/Lib/totp.ahk b/Lib/totp.ahk new file mode 100644 index 0000000..55d39bd --- /dev/null +++ b/Lib/totp.ahk @@ -0,0 +1,28 @@ + +totp(keyUri) +{ + ; Only key URI scheme is recognized + ; https://github.com/bitwarden/jslib/blob/master/src/services/totp.service.ts#L23 + if InStr(keyUri, "otpauth://totp") != 1 + return + ; https://github.com/google/google-authenticator/wiki/Key-Uri-Format + if !RegExMatch(keyUri, "(?<=secret=)\w+", secret) + return + RegExMatch(keyUri, "(?<=algorithm=)\w+", algorithm) + if algorithm not in SHA1,SHA256,SHA512 + algorithm := "SHA1" + if !RegExMatch(keyUri, "(?<=digits=)\d+", digits) + digits := 6 + if !RegExMatch(keyUri, "(?<=period=)\d+", period) + period := 30 + ; https://tools.ietf.org/html/rfc6238 + key := base32toHex(secret) + counter := Format("{:016x}", epoch() // period) + hmac := Crypt.Hash.HMAC(algorithm, counter, key, "hex") + offset := hex2dec(SubStr(hmac, 0)) * 2 + 1 + otp := hex2dec(SubStr(hmac, offset, 8)) & 0x7FFFFFFF + return SubStr(otp, -1 * digits + 1) + ; return RegExMatch(otp, "\d{" digits "}") ? otp : "ERROR" +} + +#Include diff --git a/Lib/unzip.ahk b/Lib/unzip.ahk new file mode 100644 index 0000000..9148ceb --- /dev/null +++ b/Lib/unzip.ahk @@ -0,0 +1,20 @@ + +unzip(zipFile, destination := "") +{ + loop files, % zipFile + zipFile := A_LoopFileLongPath + if !destination + SplitPath zipFile,, destination + else + { + if !attr := FileExist(destination) + FileCreateDir % destination + else if !InStr(attr, "D") + throw Exception("Destination not a directory", -1) + loop files, % destination + destination := A_LoopFileLongPath + } + shell := ComObjCreate("Shell.Application") + items := shell.Namespace(zipFile).Items + shell.Namespace(destination).CopyHere(items, 4|16) +} diff --git a/Lib/update.ahk b/Lib/update.ahk new file mode 100644 index 0000000..7cdbeb3 --- /dev/null +++ b/Lib/update.ahk @@ -0,0 +1,30 @@ + +update() +{ + if epoch := epoch() < INI.UPDATES["last-check"] + 86400 + return + if !update_isLatest() + { + MsgBox % 0x4|0x20|0x40000, % appTitle, Version is outdated`, open GitHub to download the latest? + IfMsgBox Yes + Run https://github.com/anonymous1184/bitwarden-autotype/releases/latest + } + IniWrite % " " epoch, % settings, UPDATES, last-check +} + +update_isLatest() +{ + if A_IsCompiled + FileGetVersion current, % A_ScriptFullPath + else + FileRead current, version + try + { + whr := ComObjCreate("WinHttp.WinHttpRequest.5.1") + whr.Open("GET" + , "https://raw.githubusercontent.com/anonymous1184/bitwarden-autotype/master/version" + , false), whr.Send() + return (current = RTrim(whr.ResponseText, "``r`n")) + } + return true ; Error while checking +} diff --git a/Lib/zip.ahk b/Lib/zip.ahk new file mode 100644 index 0000000..b67e28d --- /dev/null +++ b/Lib/zip.ahk @@ -0,0 +1,26 @@ + +zip(archive, files*) +{ + if !FileExist(archive) + VarSetCapacity(zHeader, 18, 0) + , file := FileOpen(archive, 0x1) + , file.Write("PK" Chr(5) Chr(6)) + , file.RawWrite(zHeader, 18) + , file.Close() + loop files, % archive + archive := A_LoopFileLongPath + shell := ComObjCreate("Shell.Application").Namespace(archive) + for i,file in files + { + total := shell.items().Count, filter := "F" + if isDir := InStr(FileExist(file), "D") + file .= RTrim(file, "\") "\*", filter .= "R" + loop files, % file, % filter + { + total++ + shell.CopyHere(A_LoopFileLongPath, 4|16) + while shell.items().Count != total + Sleep 10 + } + } +} diff --git a/README.md b/README.md index 56ef0e3..d8f0552 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,152 @@ # Bitwarden Auto-Type -A simple script written in [AutoHotkey](https://www.autohotkey.com/) that provides up to 2 hotkeys for Auto-Type in Windows applications (_similar_ to [KeePass](https://keepass.info/help/base/autotype.html)). + +A script-based, small, Open-Source Application written in [AutoHotkey][01] that provides keyboard shortcuts to auto-type usernames, passwords and Time-based One-Time Passwords ([TOTP][02]) for applications ***and*** websites, it borrows the concepts coined by [KeePass][03] but with [Bitwarden][04] as "backend". + +*This is the second release (a major rewrite), is not backwards compatible with the first release. It contains multiple improvements and doesn't require external dependencies.* + +It attempts to fullfil the applicable Top-10 user-requested features of the community: + +* [Auto-type/Autofill for logging into other desktop apps][req01]. +* [2FA when ‘unlocking’][req02]1. +* [Auto-logout after X minutes][req03]. +* [Auto-fill TOTP code][req04]. +* [Bitwarden Windows App - Add Autorun at System Startup][req05]. +* [Auto-Sync on all platforms][req07]2. +* [Support Internet Explorer][req07]3. +* [Autofill shortcut should open login window when vault is locked][req08] +* [Improve random password generation][req09]. + * [Password Generator Should Have More Character Set][req10]. + +1 Uses [an entry][07] with the Authenticator Key.
+2 The synchronization is done on schedule.
+3 Only IE 11 was tested, use title matching for others. + +## Features at glance + +~~[Wiki][05]~~ details them: + +* Auto-Type: with predefined and per-case sequences. +* Supports multiple accounts/windows per site. +* Favicons can be shown to easily distinct between sites. +* Quick 6-digit PIN and 2FA (TOTP) unlocking. +* Universal Window Platform support (Microsoft Store Apps). +* Browser support: instead of insecure extensions. +* TOTP generation: via Clipboard and/or hotkey and/or placeholder. +* Strong Password Generator with entropy indicator. +* Placeholder for smart detection of text input fields. +* [Two-Channel Auto-Type Obfuscation][06]: global/per-entry. + +## What it does? + +* Provides auto-type globally by executable/title/URL. +* Replaces the (intrinsically insecure) browser extension. +* It can use KeePass' TCATO algorithm for extra security. +* Passwords skip Clipboard (thus managers and cloud synchronization). + +## What it does NOT + +* Replace Bitwarden application (entries can't be added/edited). ## Instructions -- Download [Bitwarden CLI](https://github.com/bitwarden/cli) >= `1.9.0` -- Update accordingly the configuration. -- **Add** to your login entries: - - An `winapp://` or `app://` URL\*. - - **Optionally**, specify a custom typing sequence in the `Auto-Type Sequence` field (name can be changed in `[AUTOTYPE]` section of configuration). -\* Why `(win)app://`? both are [currently unused](https://github.com/bitwarden/jslib/blob/master/src/models/view/loginUriView.ts#L9). `winapp://` is consistent with `(ios|android)app://`. `app://` is OS agnostic (an Auto-Type for MacOS/Linux could make use of it). Protocols can be [iconified](https://github.com/bitwarden/jslib/blob/master/src/angular/components/icon.component.ts#L80) (`app://`, `macapp://`, `linuxapp://` and `winapp://`). +Setup: + +* Run the setup, edit the settings. +* Application can be found in the Start Menu. + +Portable: + +* Place [Bitwarden CLI][08] (at least `v1.11.0`) in the same directory. +* Update the settings (add the path to `bw.exe` if not in the same directory or if renamed). + +Both: + +* Add in Bitwarden login entries, *window rules* (see format below). +* **Optionally**, you can specify a custom typing sequence in the `auto-type` sequence field (name can be changed in `[SEQUENCES]` section of settings file). ## Format -- By executable name: - - `(win)app://thunderbird.exe` - matches by .exe name. -- By window title: - - `(win)app://Mail Server Password Required` - matches by window title. - - `(win)app://?title=Mail Server Password Required` - matches by window title. -- By window class: - - `(win)app://?class=MozillaDialogClass` - matches by window class. - -## What it does -- Provides Auto-Type based on the current window executable/title. -- Passwords skips clipboard manager (thus history and cloud syncronization). -## What it does NOT -- Replace Bitwarden application or browser extension. -- Provide in-memory protection mechanisms (_à la_ KeePass). - -## OTP generation -TOTP (RFC-6238) generation is optional. Following the example in Bitwarden products, it is coppied to clipboard. -- Download [oathtool](https://download.multiotp.net/tools/oathtool_2.6.2_windows/) | [7z](https://mega.nz/#!jot1QbJa!cNHICLMI1LOSTtI6wbIoy0JatkcFHJ6p0VQIUTWcmoY) | [zip](https://mega.nz/#!zglDQD5Q!1S3H3MYvG1SD2sk0pShsGUCHJvHr4eivkpTBPF9JBWU). -- Update accordingly the configuration. - -## Caveats -- UAC issues: - - Run the Auto-Type executable/script elevated. -- UIPI issues: - - Use Auto-Type as script with AutoHotkey istalled and UIA enabled. **_OR_** - - Create/import a certificate, sign the executable and place it accordingly. -- Login/unlock/sync feels sluggish. One or more of: - - Slow CPU. - - Big vault. - - Number of iterations of Key Derivation. +* By URL: + * `http://example.com` + * `https://www.example.com/path/login.html?foo=bar` + * It follows the "*Match Detection*" in use by Bitwarden. +* By executable name: + * `thunderbird.exe` + * `app://thunderbird.exe` + * `winapp://thunderbird.exe` +* By window class: + * `app://?class=MozillaDialogClass` + * `winapp://?class=MozillaDialogClass` +* By window title (partial match): + * `Mail Server Password` + * `app://Mail Server Password` + * `winapp://Mail Server Password` +* By window title (exact match): + * `app://?title=Mail Server Password Required` + * `winapp://?title=Mail Server Password Required` -## TODO -- Less fatal errors. -- Rewrite as Class for integration. -- ~~TOTP (3rd party tool or write [RFC-6238](https://tools.ietf.org/html/rfc6238) compilant).~~ ✔ +Why `winapp://` or `app://`? Both are [currently unused][09]. `winapp://` is consistent with `androidapp://` and `iosapp://` currently used. `app://` is OS agnostic (an Auto-Type for MacOS/Linux could make use of it). Protocols can be [iconified][10] (for example: `app://`, `macapp://`, `linuxapp://` and `winapp://`). + +## Known limitations + +* No x86 version: `bw.exe` is 64 bits only. +* TCATO can fail in specific sites/windows + * Temporarily disable it via tray menu + * Add an exception in Bitwarden (field `tcato`, value `off`). +* Some applications ***might*** fail to recognize auto-type: + * Use the setup version (recommended). + * Run the portable version as Administrator. +* `{SmartTab}` doesn't work with Chromium-based applications + * Normal Tab is sent. For more than one Tab use a custom `auto-type` rule. -## Out of Scope -- x86 version: bw.exe is 64bit. -- Any kind of GUI: it's a script. -- Fine-grained URL-based in-browser auto-typing: extension's job. +## TODO -## Remember -- This is a script, not a full-fledged enterprise-ready application (_i.e._ YMMV). -- No monkey business. Since is AutoHotkey, the source in the .exe can be read with Notepad (almost at the end of the file), or with [Resource Hacker](http://angusj.com/resourcehacker/) (plus, you can always use the bare script). +* Wiki !!! +* Internationalization. +* Global entry selection. +* UI for settings (perhaps). ## Help -- [Forums](https://community.bitwarden.com/) are a good starting point. -- GitHub issues for code-specific stuff. -## Thanks to -- **Kyle Spearring** for his incredible dedication to Bitwarden and its community. -- **Chris Mallett** and **Steve Gray** for AutoHotkey that had helped me to automate Windows stuff for over 10 years. +* Checkout the ~~[Wiki][05]~~. +* In Reddit look for the [/r/Bitwarden][12] sub. +* User-to-User support in Community [Forums][13]. +* GitHub [Issues][14] for app-specific problems/bugs. + +## Disclaimer + +This is a script-based utility; not a full-fledged, enterprise-ready application (_i.e._ YMMV). + +**No monkey business**. Given the nature of AutoHotkey, the source code can be found on the executable and be read with any text editor (almost at the end of the file) or better yet, with [Resource Hacker][11]; plus, the source script can always be used instead. ## Licence -- [WTFPL](http://www.wtfpl.net/about/) + +* [WTFPL][15] +* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +[01]: https://autohotkey.com/ "AutoHotkey" +[02]: https://en.wikipedia.org/wiki/Time-based_One-time_Password_algorithm "TOTP: Time-based One-Time Password" +[03]: https://keepass.info/help/base/autotype.html "KeePass Auto-type" +[04]: https://bitwarden.com "Bitwarden" +[05]: https://github.com/anonymous1184/bitwarden-autotype/wiki "Wiki not written yet" +[06]: https://keepass.info/help/v2/autotype_obfuscation.html "TCATO: Two-Channel Auto-Type Obfuscation" +[07]: https://i.imgur.com/WeddYxr.png "Entry with Authenticator Key" +[08]: https://github.com/bitwarden/cli "Bitwarden CLI" +[09]: https://github.com/bitwarden/jslib/blob/master/src/models/view/loginUriView.ts#L9 "loginUriView.ts:9" +[10]: https://github.com/bitwarden/jslib/blob/master/src/angular/components/icon.component.ts#L80 "icon.component.ts:6" +[11]: http://angusj.com/resourcehacker/ "Resource Hacker" +[12]: https://www.reddit.com/r/Bitwarden/ "Bitwarden Subreddit" +[13]: https://community.bitwarden.com/c/support/6 "Community Forums: User-to-User Support" +[14]: https://github.com/anonymous1184/bitwarden-autotype/issues "Issues" +[15]: http://www.wtfpl.net/about/ "Do What The Fuck You Want To Public License" + +[req01]: https://community.bitwarden.com/t/158 +[req02]: https://community.bitwarden.com/t/353 +[req03]: https://community.bitwarden.com/t/30 +[req04]: https://community.bitwarden.com/t/326 +[req05]: https://community.bitwarden.com/t/948 +[req06]: https://community.bitwarden.com/t/355 +[req07]: https://community.bitwarden.com/t/4431 +[req08]: https://community.bitwarden.com/t/1494 +[req09]: https://community.bitwarden.com/t/4091 +[req10]: https://community.bitwarden.com/t/82 diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..f250b69 --- /dev/null +++ b/README.txt @@ -0,0 +1,11 @@ + +The `.ini` must have the same name as the `.exe`. + +Download Bitwarden CLI (version 1.11.0 at least) +and place it in the same folder. + +To have UI Access, use the setup version in order +to bypass the User Interface Privilege Isolation. + +Project Home: +https://github.com/anonymous1184/bitwarden-autotype diff --git a/assets/7z.dll b/assets/7z.dll deleted file mode 100644 index b32d7bf..0000000 Binary files a/assets/7z.dll and /dev/null differ diff --git a/assets/7z.exe b/assets/7z.exe deleted file mode 100644 index 37b8514..0000000 Binary files a/assets/7z.exe and /dev/null differ diff --git a/assets/ahk2exe.exe b/assets/ahk2exe.exe deleted file mode 100644 index 8eece71..0000000 Binary files a/assets/ahk2exe.exe and /dev/null differ diff --git a/assets/bw-at.bin b/assets/bw-at.bin index c3af79a..c58ce05 100644 Binary files a/assets/bw-at.bin and b/assets/bw-at.bin differ diff --git a/assets/bw-at.ico b/assets/bw-at.ico new file mode 100644 index 0000000..6a9ca57 Binary files /dev/null and b/assets/bw-at.ico differ diff --git a/assets/bw-at.ini b/assets/bw-at.ini new file mode 100644 index 0000000..c628c33 Binary files /dev/null and b/assets/bw-at.ini differ diff --git a/assets/icon.ico b/assets/icon.ico deleted file mode 100644 index 06c5c04..0000000 Binary files a/assets/icon.ico and /dev/null differ diff --git a/assets/uninstall.ico b/assets/uninstall.ico new file mode 100644 index 0000000..a6a5914 Binary files /dev/null and b/assets/uninstall.ico differ diff --git a/build.ahk b/build.ahk new file mode 100644 index 0000000..4d3d3d5 --- /dev/null +++ b/build.ahk @@ -0,0 +1,43 @@ +#Warn All +#SingleInstance force + +SetWorkingDir % A_ScriptDir + +if DEBUG := InStr(DllCall("Kernel32\GetCommandLine", "Str"), "debug") + version := A_YYYY "." A_MM "." A_DD "." A_Hour A_Min +else +{ + version := FileOpen("version", 0x0).Read() + MsgBox % 0x4|0x20|0x100|0x40000, Bump?, Bump build in version? + IfMsgBox Yes + { + version := StrSplit(version, ".") + version := version[1] "." version[2] "." version[3] "." ++version[4] + FileOpen("version", 0x1, "CP0").Write(version) + } +} + +for each,script in ["bw-at", "uninstall", "setup"] +{ + buffer := FileOpen(script ".ahk", 0x0, "UTF-8").Read() + buffer := RegExReplace(buffer, "(SetVersion ).+", "$1" version) + buffer := RegExReplace(buffer, "(SetProductVersion ).+", "$1" version) + FileOpen(script ".ahk", 0x1, "UTF-8").Write(buffer) + RunWait % A_ProgramFiles "\AutoHotkey\Compiler\Ahk2Exe.exe /bin assets\bw-at.bin /in " script ".ahk", % A_WorkingDir +} + +; Portable +FileDelete release\bw-at.zip +zip("release\bw-at.zip" + , "*.txt", "LICENSE" ; Documents + , "assets\bw-at.ini" ; Template + , "bw-at.exe") ; Main Executable + +; Cleanup +FileMove setup.exe, release, % true +FileDelete *.exe + +if DEBUG + OutputDebug Done! +else + MsgBox % 0x40|0x40000, % A_Space, Build Complete! diff --git a/build.cmd b/build.cmd deleted file mode 100644 index fe0f630..0000000 --- a/build.cmd +++ /dev/null @@ -1,10 +0,0 @@ -@echo off -echo. -echo Working... -echo. -%~dp0assets\ahk2exe.exe /bin %~dp0assets\bw-at.bin /in %~dp0bw-at.ahk /out %~dp0release\bw-at.exe -pushd %~dp0release -del /q bw-at.*z* -..\assets\7z.exe a -t7z -mx=9 bw-at.7z bw-at.exe >nul -..\assets\7z.exe a -tzip -mx=9 bw-at.zip bw-at.exe >nul -pause diff --git a/bw-at.ahk b/bw-at.ahk index 826eca5..5f887af 100644 --- a/bw-at.ahk +++ b/bw-at.ahk @@ -1,582 +1,145 @@ -; File: UTF-8 no BOM -; Style: Allman + OTBS - -#NoEnv -#SingleInstance, force - -; Versioning: YY.MM.DD.build -;@Ahk2Exe-SetVersion 20.03.24.1 +/* CODE IPSA LOQUITUR + The code speaks for itself! Or at least, it should... + (I'm very sorry about my poor code-commenting skills) +*/ ; Defaults -ListLines, Off -DetectHiddenWindows, On -SetWorkingDir, % A_ScriptDir +ListLines Off +SetBatchLines -1 +SetTitleMatchMode 2 +DetectHiddenWindows On ; Environment -global bwCli := "" - , oathtool := "" - , iniFname := "" +global SESSION := "" + , settings := "" , isLocked := "" , isLogged := "" - , atFields := [] - , atWTitle := "Bitwarden Auto-Type" - -; Same-name config file -SplitPath, A_ScriptFullPath,,,, iniFname -iniFname .= ".ini" - -; Bitwarden CLI path -bwCli := A_ScriptDir "\bw.exe" -if (!FileExist(bwCli)) -{ - IniRead, bwCli, % iniFname, GENERAL, bw -} -if (err := checkExe(bwCli, "1.9.0")) -{ - MsgBox, 0x2010, % atWTitle, % "Bitwarden CLI " err - ExitApp -} -bwCli .= " --nointeraction" - -; oathtool -IniRead, oathtool, % iniFname, GENERAL, oathtool -if (err := checkExe(oathtool)) -{ - oathtool := false -} - -; Matching mode -IniRead, titleMatching, % iniFname, GENERAL, mode -if titleMatching not in 1,2,3,RegEx -{ - titleMatching := 2 -} -SetTitleMatchMode, % titleMatching - -; Auto-lock -IniRead, atAutoLock, % iniFname, GENERAL, autolock -if atAutoLock in 1,true -{ - IniRead, atIdleTime, % iniFname, GENERAL, idletime - atIdleTime := toMs(atIdleTime) - if (atIdleTime) - { - SetTimer, autoLock, % 60 * 1000 - } - else - { - MsgBox, 0x2010, % atWTitle, Invalid idle time. - ExitApp - } -} - -; Hotkeys -IniRead, hk1, % iniFname, HOTKEYS, default, 0 -IniRead, hk2, % iniFname, HOTKEYS, password, 0 -if ((!hk1 || hk1 = "ERROR") && (!hk2 || hk2 = "ERROR")) -{ - MsgBox, 0x2010, % atWTitle, No hotkeys provided. - ExitApp -} -if (hk1) -{ - IniRead, sequenceDefault, % iniFname, AUTOTYPE, default - if (!sequenceDefault || sequenceDefault = "ERROR") - { - MsgBox, 0x2010, % atWTitle, No "default" sequence. - ExitApp - } - autoTypeDefault := Func("autoType").Bind(sequenceDefault) - Hotkey, % hk1, % autoTypeDefault, UseErrorLevel - if (ErrorLevel) - { - MsgBox, 0x2010, % atWTitle, Invalid "default" hotkey. - ExitApp - } -} -if (hk2) -{ - IniRead, sequencePassword, % iniFname, AUTOTYPE, password - if (!sequencePassword || sequencePassword = "ERROR") - { - MsgBox, 0x2010, % atWTitle, No "password" sequence. - ExitApp - } - autoTypePassword := Func("autoType").Bind(sequencePassword) - Hotkey, % hk2, % autoTypePassword, UseErrorLevel - if (ErrorLevel) - { - MsgBox, 0x2010, % atWTitle, Invalid "password" hotkey. - ExitApp - } -} - -; Custom sequence field -IniRead, sequenceField, % iniFname, AUTOTYPE, field + , bwFields := [] + , bwStatus := {} + , appTitle := "Bitwarden Auto-Type" -; Tray menu -Menu, Tray, NoStandard -Menu, Tray, Icon, imageres.dll, 225 -Menu, Tray, Tip, Bitwarden Auto-Type -Menu, Tray, Add, &Sync, sync -Menu, Tray, Add, Loc&k, toggleLock -Menu, Tray, Add, &Logout, toggleLogin -Menu, Tray, Add -Menu, Tray, Add, &Exit, bye - -; Cleanup -bw("logout", 1) - -login() - -return - -autoLock() -{ - global atIdleTime - if (A_TimeIdlePhysical >= atIdleTime && !isLocked) - { - bw("lock", 1) - TrayTip, % atWTitle, Inactivity lock, 10, 0x20 - Menu, Tray, Rename, Loc&k, Unloc&k - isLocked := 1 - } -} - -autoType(sequence) +/*@Ahk2Exe-Keep +FileGetVersion version, % A_ScriptFullPath +if StrSplit(version, ".")[1] > 2020 { - if (!isLogged || isLocked) - { - return - } - active := WinExist("A") - for k,field in atFields - { - ; by .exe name - if (SubStr(field.uri, -3) = ".exe") - { - WinGet, match, ProcessName, % "ahk_id " active - match := (match = field.uri) - } - else ; by Window properties - { - if (RegExMatch(field.uri, "title=(.+)", match)) - { - WinGet, match, ID, % match1 - } - else if (RegExMatch(field.uri, "class=(.+)", match)) - { - WinGet, match, ID, % "ahk_class " match1 - } - else ; Title in plain form - { - WinGet, match, ID, % field.uri - } - match := (match = active) - } - - if (match) - { - sequence := (field.sequence ? field.sequence : sequence) - sequence := StrReplace(sequence, "%username%", field.username) - sequence := StrReplace(sequence, "%password%", field.password) - Send, % sequence - if (oathtool && field.totp) - { - totp(field.totp) - } - return ; Stop at first match - } - } + MsgBox % 0x4|0x30|0x100|0x40000, DEBUG, This is a DEBUG version`, continue? + IfMsgBox No + ExitApp 1 } +*/ -bw(params, quick := 0) +; Settings +SplitPath A_ScriptFullPath,, dir,, name +for i,file in [A_AppData "\Auto-Type\settings.ini", dir "\" name ".ini", dir "\dev\.ini"] + if FileExist(file) ~= "[^D]+" + settings := file +if !settings { - if (quick) - { - RunWait, % bwCli " " params,, Hide UseErrorLevel - return - } - Run, % A_ComSpec,, Hide, cmdPid - WinWait, % "ahk_pid " cmdPid - DllCall("AttachConsole", "UInt",cmdPid) - objShell := ComObjCreate("WScript.Shell") - objExec := objShell.Exec(bwCli " " params) - out := objExec.StdOut.ReadAll() - if (!out) - { - err := objExec.StdErr.ReadAll() - MsgBox, 0x2010, % atWTitle, There was an error:`n`n%err% - ExitApp - } - DllCall("FreeConsole") - Process Close, % cmdPid - return out -} + MsgBox % 0x10|0x40000, % appTitle, Settings file not found. + ExitApp 1 +} else if InStr(DllCall("Kernel32\GetCommandLine", "Str"), "/restart") + settings() ; Application was reloaded, look for changes -bye() -{ - ExitApp -} +; Load settings +global INI := loadIni(settings) -checkExe(path, version := 0) -{ - attribs := FileExist(path) - if (InStr(attribs, "D") || SubStr(path, -3) != ".exe") - { - return "not an executable" - } +; Error report +OnError("errorReport") - FileGetVersion, exeVersion, % path - if (version && !checkVersion(exeVersion, version)) - { - return "incompatible version" - } +; Updates at startup +opt := INI.GENERAL.updates +if opt in 1,true,yes + update() - return false -} +; Current Working Directory +SplitPath settings,, cwd +SetWorkingDir % cwd -checkVersion(base, required) +; Bitwarden CLI path +bwCli := A_WorkingDir "\bw.exe" +if !FileExist(bwCli) + bwCli := INI.GENERAL.bw +if err := checkExe(bwCli, "1.11.0") { - base := StrSplit(base, ".") - required := StrSplit(required, ".") - for i,n in required - { - n += 0 - base[i] += 0 - if (base[i] > n) - { - return true - } - else if (base[i] < n) - { - return false - } - } - return false + MsgBox % 0x10|0x40000, % appTitle, % "Bitwarden CLI: " err + ExitApp 1 } -login() -{ - ; Credentials - IniRead, bwUser, % iniFname, CREDENTIALS, user - InputBox, bwPass, % atWTitle, Master Password, HIDE, 250, 125 - if (ErrorLevel) - { - ExitApp - } - login := "login " bwUser " " bwPass - - ; OTP - IniRead, bwOTP, % iniFname, CREDENTIALS, otp, no - if bwOTP in A,E,Y - { - if (bwOTP = "E") - { - ; Trigger email - bw(login " --method 1", 1) - } - InputBox, bwOTPcode, % atWTitle, Two-step Login,, 250, 125 - if (ErrorLevel) - { - ExitApp - } - methods := {A: "0", E: "1", Y: "3"} - login .= " --method " methods[bwOTP] " --code " bwOTPcode - } - - ; Store session - EnvSet, BW_SESSION, % bw(login " --raw") - isLogged := 1 - isLocked := 0 - - ; init - parseItems() - - ; Acknowledge - TrayTip, % atWTitle, Auto-Type ready, 10, 0x20 -} +; TOTP in Clipboard +opt := INI.GENERAL.totp +if opt not in 1,true,yes,hide + INI.GENERAL.totp := false -parseItems() -{ - global sequenceField +; Auto-lock +autoLock(INI.GENERAL["auto-lock"]) - items := bw("list items") - items := Jxon_Load(items) +; Auto-logout +autoLogout(INI.GENERAL["auto-logout"]) - atFields := [] - for i,item in items - { - ; Logins - if (item.type = 1) - { - uri := item.login.uris[1].uri - if (RegExMatch(uri, "^(win)?app://(.+)", match)) - { - atFields[i] := { uri: match2 - , totp: item.login.totp - , username: item.login.username - , password: item.login.password } - ; Custom sequence - for j,field in item.fields - { - if (field.name = sequenceField) - { - atFields[i]["sequence"] := field.value - } - } - } - } - } -} +; Auto-sync +sync_auto(INI.GENERAL["auto-sync"]) -sync() -{ - if (!isLogged) - { - MsgBox, 0x2010, % atWTitle, Login first. - return - } - if (isLocked) - { - MsgBox, 0x2010, % atWTitle, Vault locked. - return - } - bw("sync") - parseItems() - TrayTip, % atWTitle, Sync complete, 10, 0x20 -} +; Favicons +opt := INI.GENERAL.favicons +if opt not in 1,true,yes + INI.GENERAL.favicons := false -totp(key) +; Username +if !INI.CREDENTIALS.user { - ; Only key URI scheme is recognized - ; https://github.com/bitwarden/jslib/blob/master/src/services/totp.service.ts#L25 - if (SubStr(key, 1, 14) = "otpauth://totp") - { - params := "--base32 --totp" - if (RegExMatch(key, "algorithm=(\w+)", match)) - { - params .= Format("={:l}", match1) - } - if (RegExMatch(key, "secret=(\w+)", match)) - { - params .= " " match1 - } - else ; Invalid, no secret - { - return - } - if (RegExMatch(key, "period=(\d+)", match)) - { - params .= " --time-step-size=" match1 - } - if (RegExMatch(key, "digits=(\d+)", match)) - { - params .= " --digits=" match1 - } - RunWait, % A_ComSpec " /c " oathtool " " params " | clip",, Hide UseErrorLevel - Clipboard := RTrim(Clipboard, "`r`n") - } + MsgBox % 0x10|0x40000, % appTitle, No username provided. + ExitApp 1 } -toggleLock() +; Hotkeys +if !INI.HOTKEYS.Count() { - if (!isLogged) - { - MsgBox, 0x2010, % atWTitle, Login first. - return - } - if (isLocked) - { - InputBox, bwPass, % atWTitle, Master Password, HIDE, 250, 125 - if (ErrorLevel) - { - ExitApp - } - EnvSet, BW_SESSION, % bw("unlock " bwPass " --raw") - TrayTip, % atWTitle, Vault unlocked, 10, 0x20 - Menu, Tray, Rename, Unloc&k, Loc&k - isLocked := 0 - } - else - { - bw("lock", 1) - TrayTip, % atWTitle, Vault locked, 10, 0x20 - Menu, Tray, Rename, Loc&k, Unloc&k - isLocked := 1 - } + MsgBox % 0x10|0x40000, % appTitle, No hotkeys provided. + ExitApp 1 } +for field,key in INI.HOTKEYS + bind(field, key) -toggleLogin() -{ - if (isLogged) - { - bw("logout", 1) - TrayTip, % atWTitle, Logged out, 10, 0x20 - Menu, Tray, Rename, &Logout, &Login - isLogged := 0 - } - else - { - login() - Menu, Tray, Rename, &Login, &Logout - } -} +; Two-Channel Auto-Type Obfuscation +opt := INI.TCATO.use +if opt not in 1,true,yes + INI.TCATO.use := false -toMs(str) +; PIN / 2fa +if !opt := INI.PIN.use + INI.PIN.use := false +else if opt in 1,true,yes + INI.PIN.use := -1 +if INI.PIN.use != -1 { - mult := 0 - r := SubStr(str, 0) - l := SubStr(str, 1, -1) - if (r = "m") - { - mult := 1000 * 60 - } - else if (r = "h") - { - mult := 1000 * 60 * 60 - } - return (l * mult) + INI.PIN.hex := false + IniWrite % "", % settings, PIN, hex } -/** - * From JSON lib for AutoHotkey - * https://github.com/cocobelgica/AutoHotkey-JSON -*/ -Jxon_Load(ByRef src, args*) -{ - static q := Chr(34) - - key := "", is_key := false - stack := [ tree := [] ] - is_arr := { (tree): 1 } - next := q . "{[01234567890-tfn" - pos := 0 - while ( (ch := SubStr(src, ++pos, 1)) != "" ) - { - if InStr(" `t`n`r", ch) - continue - if !InStr(next, ch, true) - { - ln := ObjLength(StrSplit(SubStr(src, 1, pos), "`n")) - col := pos - InStr(src, "`n",, -(StrLen(src)-pos+1)) - - msg := Format("{}: line {} col {} (char {})" - , (next == "") ? ["Extra data", ch := SubStr(src, pos)][1] - : (next == "'") ? "Unterminated string starting at" - : (next == "\") ? "Invalid \escape" - : (next == ":") ? "Expecting ':' delimiter" - : (next == q) ? "Expecting object key enclosed in double quotes" - : (next == q . "}") ? "Expecting object key enclosed in double quotes or object closing '}'" - : (next == ",}") ? "Expecting ',' delimiter or object closing '}'" - : (next == ",]") ? "Expecting ',' delimiter or array closing ']'" - : [ "Expecting JSON value(string, number, [true, false, null], object or array)" - , ch := SubStr(src, pos, (SubStr(src, pos)~="[\]\},\s]|$")-1) ][1] - , ln, col, pos) - - throw Exception(msg, -1, ch) - } - - is_array := is_arr[obj := stack[1]] - - if i := InStr("{[", ch) - { - val := (proto := args[i]) ? new proto : {} - is_array? ObjPush(obj, val) : obj[key] := val - ObjInsertAt(stack, 1, val) - - is_arr[val] := !(is_key := ch == "{") - next := q . (is_key ? "}" : "{[]0123456789-tfn") - } - - else if InStr("}]", ch) - { - ObjRemoveAt(stack, 1) - next := stack[1]==tree ? "" : is_arr[stack[1]] ? ",]" : ",}" - } - - else if InStr(",:", ch) - { - is_key := (!is_array && ch == ",") - next := is_key ? q : q . "{[0123456789-tfn" - } - - else ; string | number | true | false | null - { - if (ch == q) ; string - { - i := pos - while i := InStr(src, q,, i+1) - { - val := StrReplace(SubStr(src, pos+1, i-pos-1), "\\", "\u005C") - static end := A_AhkVersion<"2" ? 0 : -1 - if (SubStr(val, end) != "\") - break - } - if !i ? (pos--, next := "'") : 0 - continue +menu() ; Tray options +init() ; Login/unlock and parse +return ; End of auto-execute - pos := i ; update pos - val := StrReplace(val, "\/", "/") - , val := StrReplace(val, "\" . q, q) - , val := StrReplace(val, "\b", "`b") - , val := StrReplace(val, "\f", "`f") - , val := StrReplace(val, "\n", "`n") - , val := StrReplace(val, "\r", "`r") - , val := StrReplace(val, "\t", "`t") - - i := 0 - while i := InStr(val, "\",, i+1) - { - if (SubStr(val, i+1, 1) != "u") ? (pos -= StrLen(SubStr(val, i)), next := "\") : 0 - continue 2 - - ; \uXXXX - JSON unicode escape sequence - xxxx := Abs("0x" . SubStr(val, i+2, 4)) - if (A_IsUnicode || xxxx < 0x100) - val := SubStr(val, 1, i-1) . Chr(xxxx) . SubStr(val, i+6) - } - - if is_key - { - key := val, next := ":" - continue - } - } - - else ; number | true | false | null - { - val := SubStr(src, pos, i := RegExMatch(src, "[\]\},\s]|$",, pos)-pos) - - ; For numerical values, numerify integers and keep floats as is. - ; I'm not yet sure if I should numerify floats in v2.0-a ... - static number := "number", integer := "integer" - if val is %number% - { - if val is %integer% - val += 0 - } - ; in v1.1, true,false,A_PtrSize,A_IsUnicode,A_Index,A_EventInfo, - ; SOMETIMES return strings due to certain optimizations. Since it - ; is just 'SOMETIMES', numerify to be consistent w/ v2.0-a - else if (val == "true" || val == "false") - val := %val% + 0 - ; AHK_H has built-in null, can't do 'val := %value%' where value == "null" - ; as it would raise an exception in AHK_H(overriding built-in var) - else if (val == "null") - val := "" - ; any other values are invalid, continue to trigger error - else if (pos--, next := "#") - continue - - pos += i-1 - } - - is_array? ObjPush(obj, val) : obj[key] := val - next := obj==tree ? "" : is_array ? ",]" : ",}" - } - } - - return tree[1] -} +#NoEnv +#NoTrayIcon +#KeyHistory 0 +#WinActivateForce +#HotkeyInterval -1 +#SingleInstance force + +; Includes +#Include %A_ScriptDir% +#Include +;@Ahk2Exe-IgnoreBegin +#Include *i dev\warn.ahk +;@Ahk2Exe-IgnoreEnd + +;@Ahk2Exe-SetCopyright Copyleft 2020 +;@Ahk2Exe-SetDescription Bitwarden Auto-Type Executable +;@Ahk2Exe-SetLanguage 0x0409 +;@Ahk2Exe-SetMainIcon %A_ScriptDir%\assets\bw-at.ico +;@Ahk2Exe-SetName Bitwarden Auto-Type +;@Ahk2Exe-SetOrigFilename bw-at.ahk +;@Ahk2Exe-SetVersion 1.0.0.1 +;@Ahk2Exe-SetProductVersion 1.0.0.1 diff --git a/bw-at.ini b/bw-at.ini deleted file mode 100644 index 832b32c..0000000 --- a/bw-at.ini +++ /dev/null @@ -1,57 +0,0 @@ -[GENERAL] -bw = -; Bitwarden CLI location (if other than current directory) - -oathtool = oathtool-2.6.2\oathtool.exe -; oathtool location (leave blank to disable TOTP generation) - -mode = 2 -; Title Matching mode -; 1: Starts with. -; 2: Contains. -; 3: Exact match. -; RegEx: Regular expresion match. -; https://is.gd/SetTitleMatchMode - -autolock = -; Lock vault automatically -; 1/0 true/false - -idletime = 5m -; Time for autolock -; "h" suffix for hours -; "m" suffix for minutes - -[CREDENTIALS] -user = -; Bitwarden Username - -otp = -; A = Authenticator -; E = Email -; Y = Yubikey -; 0/false = Disabled -; Other methods are not supported by the CLI: -; https://help.bitwarden.com/article/cli/#enums - -[HOTKEYS] -default = ^!a -; Blank to disable - -password = ^!p -; Blank to disable - -; https://is.gd/HotkeyModifierSymbols - -[AUTOTYPE] -field = Auto-Type Sequence -; Name of the field containing a custom sequence - -default = %username%{Tab}%password%{Enter} -; Default sequence - -password = %password%{Enter} -; Password-only sequence - -; See -; https://is.gd/KeysList diff --git a/setup.ahk b/setup.ahk new file mode 100644 index 0000000..81e403e --- /dev/null +++ b/setup.ahk @@ -0,0 +1,176 @@ +; Defaults +ListLines Off +SetBatchLines -1 +DetectHiddenWindows On + +; Arguments +verbose := !quiet := DEBUG := false +for i,arg in A_Args +{ + if arg ~= "i)[-|\/]quiet" + verbose := !quiet := true + else if arg ~= "i)[-|\/]debug" + DEBUG := true +} + +; Check if latest version +if !DEBUG && !update_isLatest() +{ + SetTimer retry2yes, 1 + MsgBox % 0x5|0x40|0x40000, Download?, The version included with this installer is outdated`, do you want to go to GitHub and download the current release? + IfMsgBox Retry + Run https://github.com/anonymous1184/bitwarden-autotype/releases/latest + ExitApp +} + +; Ask +if verbose +{ + SetTimer retry2yes, 1 + MsgBox % 0x5|0x20|0x40000, Install?, Do you want to install Bitwarden Auto-Type? + IfMsgBox Cancel + ExitApp +} + +; Pre-load +latest := "" +SetTimer Preload, -1 + +; Close if running +while WinExist("ahk_exe bw-at.exe") + RunWait taskkill.exe /F /IM bw-at.exe /T,, Hide UseErrorLevel + +/* +If installing after a un-install that couldn't remove directories, those directories +are queued for deletion on the next reboot. The entries contain a double line-ending +that AHK can't handle, thus reg.exe is used to query the values and then are parsed. +*/ +clipBackup := ClipboardAll +RunWait % ComSpec " /C " quote("reg query " quote("HKLM\SYSTEM\CurrentControlSet\Control\Session Manager") " /v PendingFileRenameOperations /se * | clip"),, Hide +operations := StrSplit(Clipboard, "\??\", "*`r`n"), operations.Delete(1) +data := "", Clipboard := clipBackup +for i,file in operations + data .= InStr(file, "\Auto-Type") ?: "\??\" file "`n`n" +RegWrite REG_MULTI_SZ, HKLM\SYSTEM\CurrentControlSet\Control\Session Manager, PendingFileRenameOperations, % data + +; Settings +FileCreateDir % A_AppData "\Auto-Type" +FileInstall assets\bw-at.ini, % A_AppData "\Auto-Type\settings.ini", % false + +; App +FileCreateDir % A_ProgramFiles "\Auto-Type" +FileInstall bw-at.exe, % A_ProgramFiles "\Auto-Type\bw-at.exe", % true + +; Uninstaller +FileGetVersion version, % A_ScriptFullPath +FileInstall uninstall.exe, % A_ProgramFiles "\Auto-Type\uninstall.exe", % true +key := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Auto-Type" +RegWrite REG_SZ, % key, DisplayIcon , % quote(A_ProgramFiles "\Auto-Type\bw-at.exe") +RegWrite REG_SZ, % key, DisplayName , Bitwarden Auto-Type +RegWrite REG_SZ, % key, DisplayVersion , % version +RegWrite REG_SZ, % key, NoModify , 1 +RegWrite REG_SZ, % key, Publisher , anonymous1184 +RegWrite REG_SZ, % key, QuietUninstallString, % quote(A_ProgramFiles "\Auto-Type\uninstall.exe") " /quiet" +RegWrite REG_SZ, % key, UninstallString , % quote(A_ProgramFiles "\Auto-Type\uninstall.exe") +RegWrite REG_SZ, % key, URLInfoAbout , https://github.com/anonymous1184/bitwarden-autotype/ + +; Signatures +signExe(A_ProgramFiles "\Auto-Type\bw-at.exe", "Auto-Type", true) +signExe(A_ProgramFiles "\Auto-Type\uninstall.exe", "Auto-Type") + +; Start Menu +FileCreateDir % start := A_AppDataCommon "\Microsoft\Windows\Start Menu\Programs\Auto-Type" +FileCreateShortcut % A_ProgramFiles "\Auto-Type\bw-at.exe", % start "\Auto-Type.lnk" +FileCreateShortcut % A_ProgramFiles "\Auto-Type\uninstall.exe", % start "\Uninstall.lnk" +FileCreateShortcut % A_ProgramFiles "\Auto-Type\bw-at.exe", % A_DesktopCommon "\Auto-Type.lnk" +IniWrite https://github.com/anonymous1184/bitwarden-autotype, % start "\Project Page.url", InternetShortcut, URL +IniWrite C:\Windows\System32\shell32.dll, % start "\Project Page.url", InternetShortcut, IconFile +IniWrite 14, % start "\Project Page.url", InternetShortcut, IconIndex + +; bw.exe +if verbose +{ + MsgBox % 0x4|0x20|0x40000, Download?, Do you want to download Bitwarden CLI? + IfMsgBox No + Goto Settings +} + +; Progress +Gui New, +AlwaysOnTop +HwndHwnd +ToolWindow +Gui Font, s11 q5, Consolas +Gui Add, Text,, Getting version... +Gui Show,, > Download +Hotkey IfWinActive, % "ahk_id" hWnd +Hotkey !F4, WinExist +hMenu := DllCall("User32\GetSystemMenu", "UInt",hWnd, "UInt",0) +for i,uPosition in { SC_MINIMIZE: 0xF020, SC_CLOSE: 0xF060 } + DllCall("User32\DeleteMenu", "UInt",hMenu, "UInt",uPosition, "UInt",0) + +; Latest +while !latest + Sleep -1 +if !IsObject(latest) +{ + Gui Destroy + MsgBox % 0x1|0x10|0x40000, Error, Cannot retrieve file version information + Goto Settings +} + +; Download +SetTimer Percentage, 1 +tmpZipFile := A_Temp "\" A_Now ".zip" +UrlDownloadToFile % asset.browser_download_url, % tmpZipFile +SetTimer Percentage, Delete +Gui Destroy + +; Unzip +unzip(tmpZipFile, A_AppData "\Auto-Type") + +; Cleanup +FileDelete % tmpZipFile + +; Open Settings +Settings: + MsgBox % 0x40|0x40000, Complete!, Installation complete`, please update the seatings accordingly. + Run edit settings.ini, % A_AppData "\Auto-Type", UseErrorLevel, settingsPid + WinWait % "ahk_pid" settingsPid + WinActivate % "ahk_pid" settingsPid +ExitApp + +Preload: + whr := ComObjCreate("WinHttp.WinHttpRequest.5.1") + url := "https://api.github.com/repos/bitwarden/cli/releases/latest" + whr.Open("GET", url, false), whr.Send() + latest := JSON.Load(whr.ResponseText) + for i,asset in latest.assets + if InStr(asset.name, "windows") + break +return + +Percentage: + FileGetSize current, % tmpZipFile + GuiControl % hWnd ":", Static1, % "Downloaded: " Round(current / asset.size * 100, 2) "%" +return + +#NoEnv +#NoTrayIcon +#KeyHistory 0 +#SingleInstance force + +; Includes +#Include %A_ScriptDir% +;@Ahk2Exe-IgnoreBegin +#Include *i dev\warn.ahk +;@Ahk2Exe-IgnoreEnd +#Include +#Include + +;@Ahk2Exe-SetCopyright Copyleft 2020 +;@Ahk2Exe-SetDescription Bitwarden Auto-Type Installer +;@Ahk2Exe-SetLanguage 0x0409 +;@Ahk2Exe-SetMainIcon %A_ScriptDir%\assets\bw-at.ico +;@Ahk2Exe-SetName Bitwarden Auto-Type +;@Ahk2Exe-SetOrigFilename setup.ahk +;@Ahk2Exe-SetVersion 1.0.0.1 +;@Ahk2Exe-SetProductVersion 1.0.0.1 +;@Ahk2Exe-UpdateManifest 1 diff --git a/uninstall.ahk b/uninstall.ahk new file mode 100644 index 0000000..9453319 --- /dev/null +++ b/uninstall.ahk @@ -0,0 +1,90 @@ +; Defaults +ListLines Off +SetBatchLines -1 +DetectHiddenWindows On + +; Level +if A_Args[1] ~= "i)[-|\/]quiet" + verbose := !quiet := true + +if verbose +{ + SetTimer retry2yes, 1 + MsgBox % 0x5|0x30|0x40000, Uninstall?, Do you want to uninstall Bitwarden Auto-Type? + IfMsgBox Cancel + ExitApp +} + +while WinExist("ahk_exe bw-at.exe") +{ + if verbose + { + SetTimer retry2yes, 1 + MsgBox % 0x5|0x20|0x40000, Close?, Application is running`, close it before continuing? + IfMsgBox Cancel + ExitApp + } + RunWait taskkill /F /IM bw-at.exe /T,, Hide UseErrorLevel +} + +settings := 1 +if verbose +{ + MsgBox % 0x4|0x20|0x40000, Remove?, Do you want to remove the stored settings? + IfMsgBox No + settings := 0 +} + +; Execute from %Temp% +if !InStr(A_ScriptFullPath, A_Temp) +{ + tmp := A_Temp "\" A_Now ".exe" + FileCopy % A_ScriptFullPath, % tmp, % true + Run % tmp " /quiet /s:" settings + ExitApp +} + +; Folders to remove +dirs := [ A_AppData "\Auto-Type" + , A_ProgramFiles "\Auto-Type" + , A_AppDataCommon "\Microsoft\Windows\Start Menu\Programs\Auto-Type"] + +if A_Args[2] ~= "s:0" + dirs.RemoveAt(1) + +for i,dir in dirs +{ + FileRemoveDir % dir, % true + if ErrorLevel + DllCall("Kernel32\MoveFileEx", "Str",dir, "Int",0, "UInt",0x4) +} +FileDelete % A_DesktopCommon "\Auto-Type.lnk" + +signExe_DeleteCert("Auto-Type") +RegDelete HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Auto-Type +RegDelete HKCU\Software\Microsoft\Windows\CurrentVersion\Run, Bitwarden Auto-Type + +MsgBox % 0x40|0x40000, Success!, Bitwarden Auto-Type has been successfully uninstalled. +Run % ComSpec " /C " quote("timeout /t 1 & del " quote(A_ScriptFullPath)),, Hide + +#NoEnv +#NoTrayIcon +#KeyHistory 0 +#SingleInstance force + +; Includes +#Include %A_ScriptDir% +;@Ahk2Exe-IgnoreBegin +#Include *i dev\warn.ahk +;@Ahk2Exe-IgnoreEnd +#Include + +;@Ahk2Exe-SetCopyright Copyleft 2020 +;@Ahk2Exe-SetDescription Bitwarden Auto-Type Uninstaller +;@Ahk2Exe-SetLanguage 0x0409 +;@Ahk2Exe-SetMainIcon %A_ScriptDir%\assets\uninstall.ico +;@Ahk2Exe-SetName Bitwarden Auto-Type +;@Ahk2Exe-SetOrigFilename uninstall.ahk +;@Ahk2Exe-SetVersion 1.0.0.1 +;@Ahk2Exe-SetProductVersion 1.0.0.1 +;@Ahk2Exe-UpdateManifest 1 diff --git a/version b/version new file mode 100644 index 0000000..217625a --- /dev/null +++ b/version @@ -0,0 +1 @@ +1.0.0.1 \ No newline at end of file