From b2ab4ed1c89aa0e80d2087b23590f4c272114783 Mon Sep 17 00:00:00 2001 From: Rodrigo Ribeiro Gomes Date: Fri, 27 Dec 2024 01:18:00 -0300 Subject: [PATCH] Enhanced export/import encryption. Added derived keys and salt. Closes #29 --- powershai/lib/util.ps1 | 187 +++++++++++++++++++---- powershai/powershai.psm1 | 182 +++++++++++++--------- tests/pester/001-core/001-util.tests.ps1 | 17 +++ 3 files changed, 281 insertions(+), 105 deletions(-) diff --git a/powershai/lib/util.ps1 b/powershai/lib/util.ps1 index 280e47e..4f6bab8 100644 --- a/powershai/lib/util.ps1 +++ b/powershai/lib/util.ps1 @@ -317,6 +317,44 @@ function RegArgCompletion { } } +function GetParams($FunctionName){ + + $c = Get-Command $FunctionName; + $ParamsAst = $Command.ScriptBlock.Ast.Parameters; + + if(!$ParamsAst){ + $ParamsAst = $c.ScriptBlock.Ast.Body.ParamBlock.Parameters; + } + + $CommandHelp = get-help $c; + + $Global:HelpIndex = @{} + + $CommandHelp.parameters.parameter | %{ + $AllText = $_.description | %{ + + $_.text -split '\r?\n' | ?{$_ -and $_.trim()} | %{ $_.trim() } + } + + $HelpIndex[$_.name] = $AllText + + } + + foreach($param in $ParamsAst){ + $ParamName = $param.name.toString() -replace '^\$',''; + [PsCustomObject]@{ + name = $ParamName + definition = $param.toString() + help = $HelpIndex[$ParamName] + source = $c + } + + } + +} + + + #Thanks from: https://www.powershellgallery.com/packages/DRTools/4.0.2.3/Content/Functions%5CInvoke-AESEncryption.ps1 function Invoke-AESEncryption { [CmdletBinding()] @@ -413,54 +451,143 @@ function Invoke-AESEncryption { } - -function GetParams($FunctionName){ - - $c = Get-Command $FunctionName; - $ParamsAst = $Command.ScriptBlock.Ast.Parameters; +# Stronger Version! +function Invoke-AESEncryptionV2 { + [CmdletBinding()] + Param + ( + [ValidateSet('Encrypt', 'Decrypt')] + [String]$Mode, + [String]$Key, + [String]$Text + ,[switch]$DebugData + ) - if(!$ParamsAst){ - $ParamsAst = $c.ScriptBlock.Ast.Body.ParamBlock.Parameters; + # Helper function to generate a secure random byte array + function Generate-RandomBytes($size) { + $randomBytes = New-Object byte[] $size + [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($randomBytes) + return $randomBytes } + + + + $shaManaged = New-Object System.Security.Cryptography.SHA256Managed + $aesManaged = New-Object System.Security.Cryptography.AesManaged + $aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC + $aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros + $aesManaged.BlockSize = 128 + + + $EncryptedResult = $null; - $CommandHelp = get-help $c; - - $Global:HelpIndex = @{} + try { + switch ($Mode) { + 'Encrypt' { + + # Key derivation + $SaltBytes = Generate-RandomBytes 16 + $keyDerivation = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Key, $SaltBytes, 10000) + $EncryptKey = $keyDerivation.GetBytes(32) # 32 bytes = 256bits! - $CommandHelp.parameters.parameter | %{ - $AllText = $_.description | %{ + $plainBytes = [System.Text.Encoding]::UTF8.GetBytes($Text) + + # keys and encrytpr! + $aesManaged.KeySize = $EncryptKey.length*8 + $aesManaged.Key = $EncryptKey + $aesManaged.GenerateIV() + $encryptor = $aesManaged.CreateEncryptor() + + + # encrypt data! + $DataEncrypted = $encryptor.TransformFinalBlock($plainBytes, 0, $plainBytes.Length) + $encryptedBytes = $SaltBytes + $aesManaged.IV + $DataEncrypted + $EncryptedResult = [System.Convert]::ToBase64String($encryptedBytes) + + #PWSHAICV2: SALT(16bytes) + IV(16bytes) + DATA + $result = [PsCustomObject]@{ + salt = $SaltBytes + iv = $aesManaged.IV + enc = $DataEncrypted + plain = $plainBytes + full = $EncryptedResult + key = $EncryptKey + } + + $result | Add-Member -Force ScriptMethod ToString { $d = $this.full; return "PWSHAICV2:$d" } + + if($DebugData){ + return $result; + } + + return ''+$result; + } + + 'Decrypt' { + + if($Text -match 'PWSHAICV2:(.+)'){ + $Text = $matches[1]; + } else { + return $null; + } - $_.text -split '\r?\n' | ?{$_ -and $_.trim()} | %{ $_.trim() } - } - - $HelpIndex[$_.name] = $AllText - - } - - foreach($param in $ParamsAst){ - $ParamName = $param.name.toString() -replace '^\$',''; - [PsCustomObject]@{ - name = $ParamName - definition = $param.toString() - help = $HelpIndex[$ParamName] - source = $c + $EncryptedBytes = [System.Convert]::FromBase64String($Text) + $SaltBytes = $EncryptedBytes[0..15] + $aesManaged.IV = $EncryptedBytes[16..31] + $EncryptedLast = $EncryptedBytes.length - 1; + $EncryptedData = $encryptedBytes[32..$EncryptedLast] + + $keyDerivation = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($Key, $SaltBytes, 10000) + $DecryptKey = $keyDerivation.GetBytes(32) # 32 bytes = 256bits! + $aesManaged.KeySize = $DecryptKey.length*8 + $aesManaged.Key = $DecryptKey + + + $decryptor = $aesManaged.CreateDecryptor() + $DecryptedBytes = $decryptor.TransformFinalBlock($EncryptedData, 0, $EncryptedData.Length) + + $DecryptedData = [System.Text.Encoding]::UTF8.GetString($DecryptedBytes).Trim([char]0); + + $result = [PsCustomObject]@{ + salt = $SaltBytes + iv = $aesManaged.IV + enc = $EncryptedData + plain = $DecryptedBytes + data = $DecryptedData + key = $DecryptKey + } + + if($DebugData){ + return $result; + } + + return $result.data; + } } - + + } finally { + $shaManaged.Dispose() + $aesManaged.Dispose() } - } function PowerShaiEncrypt { param($str, $password) - Invoke-AESEncryption -Mode Encrypt -text $str -key $password + Invoke-AESEncryptionV2 -Mode Encrypt -text $str -key $password } function PowerShaiDecrypt { param($str, $password) - Invoke-AESEncryption -Mode Decrypt -text $str -key $password + $cmd = "Invoke-AESEncryption" + + if($str -like "PWSHAICV2:*"){ + $cmd = "Invoke-AESEncryptionV2" + } + + & $Cmd -Mode Decrypt -text $str -key $password } function PowershaiHash { diff --git a/powershai/powershai.psm1 b/powershai/powershai.psm1 index dcb6027..49b603a 100644 --- a/powershai/powershai.psm1 +++ b/powershai/powershai.psm1 @@ -1760,47 +1760,48 @@ function Invoke-AiChatTools { } } -<# - .SYNOPSIS - Exporta as configurações da sessão atual para um arquivo, criptografado por uma senha - - .DESCRIPTION - Este cmdlet é útil para salvar configurações, como os Tokens, de maneira segura. - Ele solicia uma senha e usa ela para criar um hash e criptografar os dados de configuração da sessão em AES256. - - As configurações exportadas são todas aquelas definidas na variável $POWERSHAI_SETTINGS. - Essa variável é uma hashtable contendo todos os dados configurados pelos providers, o que inclui os tokens. - - Por padrão, os chats não são exportados devido a quantidade de dados envolvidos, o que pode deixar o arquivo muito grande! - - O arquivo exportado é salvo em um diretório criado automaticamente, por padrão, na home do usuário ($HOME). - Os objetos são exportados via Serialization, que é o mesmo método usado por Export-CliXml. - - Os dados são exportados em um formato próprio que pode ser importado apenas com Import-PowershaiSettings e informando a mesma senha. - - Uma vez que o PowershAI não faz um export automático, é recomendo invocar esse comando comando sempre que houver alteração de configuração, como a inclusão de novos tokens. - - O diretório de export pode ser qualquer caminho válido, incluindo cloud drives como OneDrive,Dropbox, etc. - - Este comando foi criado com o intuito de ser interativo, isto é, precisa da entrada do usuário em teclado. - - .EXAMPLE - # Exportando as configurações padrões! - > Export-PowershaiSettings - - + +function Export-PowershaiSettings { + <# + .SYNOPSIS + Exporta as configurações da sessão atual para um arquivo, criptografado por uma senha + + .DESCRIPTION + Este cmdlet é útil para salvar configurações, como os Tokens, de maneira segura. + Ele solicia uma senha e usa ela para criar um hash e criptografar os dados de configuração da sessão em AES256. + + As configurações exportadas são todas aquelas definidas na variável $POWERSHAI_SETTINGS. + Essa variável é uma hashtable contendo todos os dados configurados pelos providers, o que inclui os tokens. + + Por padrão, os chats não são exportados devido a quantidade de dados envolvidos, o que pode deixar o arquivo muito grande! + + O arquivo exportado é salvo em um diretório criado automaticamente, por padrão, na home do usuário ($HOME). + Os objetos são exportados via Serialization, que é o mesmo método usado por Export-CliXml. + + Os dados são exportados em um formato próprio que pode ser importado apenas com Import-PowershaiSettings e informando a mesma senha. + + Uma vez que o PowershAI não faz um export automático, é recomendo invocar esse comando comando sempre que houver alteração de configuração, como a inclusão de novos tokens. + + O diretório de export pode ser qualquer caminho válido, incluindo cloud drives como OneDrive,Dropbox, etc. - .EXAMPLE - # Exporta tudo, incluindo os chats! - > Export-PowershaiSettings -Chat - - .EXAMPLE - # Exportando para o OneDrive - > $Env:POWERSHAI_EXPORT_DIR = "C:\Users\MyUserName\OneDrive\Powershai" - > Export-PowershaiSettings + Este comando foi criado com o intuito de ser interativo, isto é, precisa da entrada do usuário em teclado. -#> -function Export-PowershaiSettings { + .EXAMPLE + # Exportando as configurações padrões! + > Export-PowershaiSettings + + + + .EXAMPLE + # Exporta tudo, incluindo os chats! + > Export-PowershaiSettings -Chat + + .EXAMPLE + # Exportando para o OneDrive + > $Env:POWERSHAI_EXPORT_DIR = "C:\Users\MyUserName\OneDrive\Powershai" + > Export-PowershaiSettings + + #> [CmdletBinding()] param( #Diretório de export @@ -1841,6 +1842,18 @@ function Export-PowershaiSettings { } } + # Remove all settings starting with "_" + @($ExportData.settings.keys) | %{ + if($_ -like "_*"){ + verbose "Removing temporary setting $_"; + $ExportData.settings.remove($_) + + if($ExportData.current -eq $_){ + $ExportData.current = "default" + } + } + } + write-verbose "Serializaing..." $Serialized = [System.Management.Automation.PSSerializer]::Serialize($ExportData); $Decrypted = @( @@ -1857,42 +1870,43 @@ function Export-PowershaiSettings { write-host "Exported to: $ExportFile"; } -<# - .SYNOPSIS - Importa uma configuração exportada com Export-PowershaiSettings - - .DESCRIPTION - Este cmdlet é o pair do Export-PowershaiSettings, e como o nome indica, ele importa os dados que foram exportados. - Você deve garantir que a mesma senha e o mesmo arquivo são passados. - - IMPORTANTE: Este comando sobscreverá todos os dados configurados na sessão. Só execute ele se tiver certeza absoluta que nenhum dado configurado previamente pode ser perdido. - Por exemplo, alguma API Token nova gerada recentemente. - - Se você estivesse especifciado um caminho de export diferente do padrão, usando a variável POWERSHAI_EXPORT_DIR, deve usa ro mesmo aqui. - - O processo de import valida alguns headers para garantir que o dado foi descriptografado corretamente. - Se a senha informanda estiver incorreta, os hashs não vão ser iguais, e ele irá disparar o erro de senha incorreta. - - Se, por outro lado, um erro de formado invalido de arquivo for exibido, significa que houve alguma corrupção no proesso de import ou é um bug deste comando. - Neste caso, você pode abrir uma issue no github relatando o problema. - - A partir da versão 0.7.0, um novo arquivo será gerado, chamado exportsession-v2.xml. - O arquivo antigo será mantido para que o usuário pode recuperar eventuais credenciais, se necessário. - - .EXAMPLE - # Import padrão - > Import-PowershaiSettings - - - - .EXAMPLE - # Importando do OneDrive - > $Env:POWERSHAI_EXPORT_DIR = "C:\Users\MyUserName\OneDrive\Powershai" - > Import-PowershaiSettings - - Importa as configurações que foram exportadas para um diretório alternativo (one drive). -#> + function Import-PowershaiSettings { + <# + .SYNOPSIS + Importa uma configuração exportada com Export-PowershaiSettings + + .DESCRIPTION + Este cmdlet é o pair do Export-PowershaiSettings, e como o nome indica, ele importa os dados que foram exportados. + Você deve garantir que a mesma senha e o mesmo arquivo são passados. + + IMPORTANTE: Este comando sobscreverá todos os dados configurados na sessão. Só execute ele se tiver certeza absoluta que nenhum dado configurado previamente pode ser perdido. + Por exemplo, alguma API Token nova gerada recentemente. + + Se você estivesse especifciado um caminho de export diferente do padrão, usando a variável POWERSHAI_EXPORT_DIR, deve usa ro mesmo aqui. + + O processo de import valida alguns headers para garantir que o dado foi descriptografado corretamente. + Se a senha informanda estiver incorreta, os hashs não vão ser iguais, e ele irá disparar o erro de senha incorreta. + + Se, por outro lado, um erro de formado invalido de arquivo for exibido, significa que houve alguma corrupção no proesso de import ou é um bug deste comando. + Neste caso, você pode abrir uma issue no github relatando o problema. + + A partir da versão 0.7.0, um novo arquivo será gerado, chamado exportsession-v2.xml. + O arquivo antigo será mantido para que o usuário pode recuperar eventuais credenciais, se necessário. + + .EXAMPLE + # Import padrão + > Import-PowershaiSettings + + + + .EXAMPLE + # Importando do OneDrive + > $Env:POWERSHAI_EXPORT_DIR = "C:\Users\MyUserName\OneDrive\Powershai" + > Import-PowershaiSettings + + Importa as configurações que foram exportadas para um diretório alternativo (one drive). + #> [CmdletBinding()] param( $ExportDir = $Env:POWERSHAI_EXPORT_DIR @@ -2208,7 +2222,25 @@ set-alias ajudai Get-PowershaiHelp set-alias iajuda Get-PowershaiHelp - +function Get-PowershaiSettingsSize { + param($setting) + + function Len(){ + param($o) + + [System.Management.Automation.PSSerializer]::Serialize($o).Length + } + + $Current = $POWERSHAI_SETTINGS_V2 + + if($setting){ + $setting.split(".") | %{ + $Current = $Current.$_ + } + } + + $Current.GetEnumerator() | %{ [PsCustomObject]@{ key = $_.key; length = (len $_.value) } } +} diff --git a/tests/pester/001-core/001-util.tests.ps1 b/tests/pester/001-core/001-util.tests.ps1 index 059e780..bb22b4b 100644 --- a/tests/pester/001-core/001-util.tests.ps1 +++ b/tests/pester/001-core/001-util.tests.ps1 @@ -203,6 +203,23 @@ Describe "Util Commands" -Tag "basic","utils" { } } + + + Context "Encryption V2" { + + It "Simple Encrypt/Decrypt" { + [string]$SecretData = @(1..5000|%{[Guid]::NewGuid().Guid}) -Join "`n" + [string]$RandomKey = [Guid]::NewGuid(); + + $encrypted = Invoke-AESEncryptionV2 -Mode Encrypt -Key $RandomKey -Text $SecretData + $decrypted = Invoke-AESEncryptionV2 -Mode Decrypt -Key $RandomKey -Text $encrypted + + $decrypted | Should -Be $SecretData; + } + + + } + }