Introduction
Who am I?
I'm simply an amature, hobby programmer who like to create and share applications that I make. I've been programming in multiple languages including Java, PHP, HTML/CSS, JS, C# and more. The code compared to what a professional would write will most likely be huge. There will be mistakes, maybe not completely optimized in the best way, but I'm sure that a lot of people can still learn a lot from this.
I feel like PowerShell is very underrated in terms of what it can acheive that other languages can. I write all my code by hand and not in any editor like Sapien or any of such kind. Just simply Notepad.
What will this article tell you?
In this article, I'll demonstrate, and show how I made an application that stores and encrypts passwords.
The Encryption (AES)
For this article, I wont go into detail on the encryption part. I'll focus on building the application.
If you wish to learn more about it, I'd recommend you to read this great Stick Figure Guide on AES
Creating the application
The application, I used the following assemblys. I just put it at the top of the script.
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Security")
[void] [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
[void] [System.Windows.Forms.Application]::EnableVisualStyles()
After this, I started with building the Form
Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.form_properties(v=vs.110).aspx
$form = New-Object System.Windows.Forms.Form -Property @{
Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Windows\System32\certutil.exe")
Text = "Keep passwords safe & encrypted - Powershell"
Size = New-Object System.Drawing.Size (555,340)
FormBorderStyle = "FixedDialog"
MaximizeBox = $false
KeyPreview = $true
}
Just simply to test the form, I also added the following
[System.Windows.Forms.Application]::Run($form)
This could've also have been done by writing "$form.ShowDialog()" but I didn't and I'll come back to why.
After I got the size right, and the looks as I wanted. As my next progress I added the main componant. The DataGridView to display all the text boxes and buttons.
Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.datagridview(v=vs.110).aspx
$grid = New-Object System.Windows.Forms.DataGridView -Property @{
Size = New-Object System.Drawing.Size (300,250)
BackgroundColor = "Gray"
ColumnHeadersHeightSizeMode = "AutoSize"
AutoSizeRowsMode = "AllCells"
CellBorderStyle = "None"
RowHeadersVisible = $false
ReadOnly = $true
AllowUserToAddRows = $False
AllowUserToDeleteRows = $False
Dock = "Top"
GridColor = "Black"
}
$form.Controls.Add($grid)
First I docked the Control entirely to Fill the Form, but I changed my mind when I later on wanted to add the textboxes below the control. So I set it to Dock at the Top and I just changed it's size to the appropriate height.
Also modified a lot of properties as I wanted to have the look and features I wanted.
To create the columns to the DataGridView, I made a function for this to manually create them as I wanted.
function CreateColumns()
{
$column = New-Object System.Windows.Forms.DataGridViewTextBoxColumn -Property @{
HeaderText = "Name"
Width = 180
}
$grid.Columns.Add($column) | Out-Null
$column = New-Object System.Windows.Forms.DataGridViewTextBoxColumn -Property @{
HeaderText = "Password"
Width = 180
}
$grid.Columns.Add($column) | Out-Null
$column = New-Object System.Windows.Forms.DataGridViewButtonColumn -Property @{
HeaderText = "View"
Width = 50
}
$grid.Columns.Add($column) | Out-Null
$column = New-Object System.Windows.Forms.DataGridViewButtonColumn -Property @{
HeaderText = "Copy"
Width = 50
}
$grid.Columns.Add($column) | Out-Null
$column = New-Object System.Windows.Forms.DataGridViewButtonColumn -Property @{
HeaderText = "Delete"
Width = 50
}
$grid.Columns.Add($column) | Out-Null
}
I create first a TextBox column, then another TextBox column followed by 3 Button Columns.
So now whenever I add a row to the DataGridView I get a row with 2 textboxes followed by 3 buttons. Just like the image above shows.
After this. I added the rest of the controls below
Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.textbox(v=vs.110).aspx
$textBoxMaster = New-Object System.Windows.Forms.TextBox -Property @{
Location = New-Object System.Drawing.Size (13,253)
Size = New-Object System.Drawing.Size (403,20)
PasswordChar = '•'
}
$form.Controls.Add($textBoxMaster)
$textBoxName = New-Object System.Windows.Forms.TextBox -Property @{
Location = New-Object System.Drawing.Size (13,276)
Size = New-Object System.Drawing.Size (200,20)
}
$form.Controls.Add($textBoxName)
$textBoxPass = New-Object System.Windows.Forms.TextBox -Property @{
Location = New-Object System.Drawing.Size (216,276)
Size = New-Object System.Drawing.Size (200,20)
PasswordChar = '•'
}
$form.Controls.Add($textBoxPass)
Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.label(v=vs.110).aspx
$labelMaster = New-Object System.Windows.Forms.Label -Property @{
Location = New-Object System.Drawing.Size (450,256)
Size = New-Object System.Drawing.Size (100,20)
Text = "Master key"
}
$form.Controls.Add($labelMaster)
Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.button(v=vs.110).aspx
$buttonAdd = New-Object System.Windows.Forms.Button -Property @{
Location = New-Object System.Drawing.Size (430,276)
Size = New-Object System.Drawing.Size (100,20)
Text = "New secret"
}
$form.Controls.Add($buttonAdd)
I also added a timer, why I'll go into later in the events section. Full properties information can be found here: https://msdn.microsoft.com/en-us/library/system.windows.forms.timer(v=vs.110).aspx
$timer = New-Object System.Windows.Forms.Timer -Property @{
Interval = 3000
Enabled = $false
}
Now when the Form is complete, the applications looks great. But has no function what so ever.
What we need is events, stuff to happen when we press the button/enter text into the textboxes.
Before this however, I added a great piece of code that I used for the encryption/decryption part.
Credits for this all goes to https://gallery.technet.microsoft.com/PowerShell-Script-410ef9df I have just done some minor updates to the code to be compatible with this application.
function Encrypt-String($String, $Passphrase, $salt="SaltCrypto", $init="PassKeeper", [switch]$arrayOutput)
{
# Info: More information and good documentation on these functions can be found in the link above
$r = New-Object System.Security.Cryptography.RijndaelManaged
$pass = [Text.Encoding]::UTF8.GetBytes($Passphrase)
$salt = [Text.Encoding]::UTF8.GetBytes($salt)
$r.Key = (New-Object Security.Cryptography.PasswordDeriveBytes $pass, $salt, "SHA1", 5).GetBytes(32)
$r.IV = (New-Object Security.Cryptography.SHA1Managed).ComputeHash( [Text.Encoding]::UTF8.GetBytes($init) )[0..15]
$c = $r.CreateEncryptor()
$ms = New-Object IO.MemoryStream
$cs = New-Object Security.Cryptography.CryptoStream $ms,$c,"Write"
$sw = New-Object IO.StreamWriter $cs
$sw.Write($String)
$sw.Close()
$cs.Close()
$ms.Close()
$r.Clear()
[byte[]]$result = $ms.ToArray()
return [Convert]::ToBase64String($result)
}
function Decrypt-String($Encrypted, $Passphrase, $salt="SaltCrypto", $init="PassKeeper")
{
if($Encrypted -is [string])
{
$Encrypted = [Convert]::FromBase64String($Encrypted)
}
$r = New-Object System.Security.Cryptography.RijndaelManaged
$pass = [Text.Encoding]::UTF8.GetBytes($Passphrase)
$salt = [Text.Encoding]::UTF8.GetBytes($salt)
$r.Key = (New-Object Security.Cryptography.PasswordDeriveBytes $pass, $salt, "SHA1", 5).GetBytes(32) #256/8
$r.IV = (New-Object Security.Cryptography.SHA1Managed).ComputeHash( [Text.Encoding]::UTF8.GetBytes($init) )[0..15]
$d = $r.CreateDecryptor()
$ms = New-Object IO.MemoryStream @(,$Encrypted)
$cs = New-Object Security.Cryptography.CryptoStream $ms,$d,"Read"
$sr = New-Object IO.StreamReader $cs
$text = $sr.ReadToEnd()
$sr.Close()
$cs.Close()
$ms.Close()
$r.Clear()
return $text
}
OK, after I added these major Functions to the code I made the events.
First off, the button at the bottom.
$buttonAdd.Add_Click({
if(($textBoxName.Text.Length -gt 0) -and ($textBoxPass.Text.Length -gt 0) -and ($textBoxMaster.Text.Length -gt 0))
{
$grid.Rows.Add(($textBoxName.Text), (Encrypt-String -Passphrase $textBoxMaster.Text -String ($textBoxPass.Text) -salt ($textBoxName.Text) -init "$($textBoxName.Text.Length)"))
$textBoxName.Text = ""
$textBoxPass.Text = ""
$grid.ClearSelection()
Export
}
})
All the 3 fields at the bottom has to have a text length of greater than 0 for the event to actually do something. Then it creates a row, first column with the text from the textBoxName control. And in the second column the encryption goes. Doesn't matter if this is visible or not. You would still need the master key to decrypt it.
After this, an event for all the buttons in the GridDataView. I use the event called CellContentClick - see https://msdn.microsoft.com/en-us/library/system.windows.forms.datagridview.cellcontentclick(v=vs.110).aspx
$grid.Add_CellContentClick({
if($grid.CurrentCell.ColumnIndex -ge 2 -and (!$timer.Enabled))
{ # If any of the ButtonColumns are pressed
switch($grid.CurrentCell.ColumnIndex)
{
2 # View column
{
try
{
$decrypt = (Decrypt-String -Encrypted ($grid[1,$grid.CurrentCell.RowIndex].Value) -Passphrase $textBoxMaster.Text -salt ($grid[0,$grid.CurrentCell.RowIndex].Value) -init "$(($grid[0,$grid.CurrentCell.RowIndex].Value).Length)")
$global:tempArray = @($grid[1,$grid.CurrentCell.RowIndex].Value, $decrypt, $grid.CurrentCell.RowIndex)
$grid[1,$grid.CurrentCell.RowIndex].Value = $decrypt
$timer.Enabled = $true
} catch { $textBoxName.Text = "Invalid master key, unable to decrypt." }
}
3 # Copy column
{
try
{
Decrypt-String -Encrypted ($grid[1,$grid.CurrentCell.RowIndex].Value) -Passphrase $textBoxMaster.Text -salt ($grid[0,$grid.CurrentCell.RowIndex].Value) -init "$(($grid[0,$grid.CurrentCell.RowIndex].Value).Length)" | clip
} catch { $textBoxName.Text = "Invalid master key, unable to decrypt." }
}
4 # Delete column
{
$grid.Rows.Remove($grid.Rows[$grid.CurrentCell.RowIndex])
Export
}
}
}
$grid.ClearSelection()
})
This triggers every click in the datagridview, but I only choose to be interested in clicks from the 3 last columns (the buttons). I used a switch statement to see which button was pressed and coded what happend on every button.
The View column sets a variable called "decrypt" to the decrypted text, and an array of the original encrypted text. Then changes the same rows 2nd column to the visible text and enables a timer that changes back the textbox to the original encrypted text after 3 seconds.
$timer.Add_Tick({
try
{
$grid[1, $Global:tempArray[2]].Value = $Global:tempArray[0]
} catch { }
$timer.Enabled = $false
})
The Copy button simply decrypts the encrypted text and I pipelines it clip.exe to hold it onto as Ctrl+C
And finaly the delete button removes the selected Row.
I also made a feature to save and export the keys so you dont have to reenter the passwords everytime the application reopens.
function Export()
{
# Info: Exports all relevent cells to a .dat file in the %temp% directory
if(Test-Path "$env:LOCALAPPDATA\Temp\PassKeeper.dat")
{
Remove-Item "$env:LOCALAPPDATA\Temp\PassKeeper.dat" -Force -Confirm:$false | Out-Null
}
for($i=0; $i -lt $grid.RowCount; $i++)
{
$item = $grid.Rows[$i].Cells.Value
"$($item[0])→$($item[1])" | Out-File "$env:LOCALAPPDATA\Temp\PassKeeper.dat" -Append
}
}
function Import()
{
# Info: Imports .dat file, if it exists to retreive old saved secrets
if(Test-Path "$env:LOCALAPPDATA\Temp\PassKeeper.dat")
{
$grid.RowCount = 0
GC "$env:LOCALAPPDATA\Temp\PassKeeper.dat" | % {
$grid.Rows.Add("$($_.Split("→")[0])", "$($_.Split("→")[1])")
}
}
}
The Import function is called at the start of the application, and the Export everytime the datagridview changes in any way.
The functions work in the way that they checks every row and collects the data of the column1 and column2 and appends this to a file with an " → " for an easy way to split this back in the application in the Import function.
Import reads the file, if it exists and creates a row in the datagridview from every row in the file split by the " → " character.
Another function I like to use is one to hide the Console window so that it's not visible.
function HideConsole($show=$false)
{
# Info: Hides the console window
# Params: show, if I would like to show the console again. Just add the parameter -show:$true
if($show) { $v = 5 } else { $v = 0 }
Add-Type -Name Window -Namespace Console -MemberDefinition '
[DllImport("Kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, Int32 nCmdShow);
'
$consolePtr = [Console.Window]::GetConsoleWindow()
[Console.Window]::ShowWindow($consolePtr, $v)
}
I call this Function at the start of the application in order to hide the console window.
Also for a finish touch, I added a NotifyIcon together with if you minimize the application. It hides from the taskbar. Along with a contextmenu on the NotifyIcon to show it again or Exit the application.
$IconMenu = New-Object System.Windows.Forms.ContextMenu
$IconMenuExit = New-Object System.Windows.Forms.MenuItem
$IconMenuShow = New-Object System.Windows.Forms.MenuItem -Property @{
Text = "Show"
}
$IconMenuExit.Text = "E&xit"
$IconMenu.MenuItems.Add($IconMenuShow) | Out-Null
$IconMenu.MenuItems.Add($IconMenuExit) | Out-Null
$Notify = New-Object System.Windows.Forms.NotifyIcon -Property @{
Icon = [System.Drawing.Icon]::ExtractAssociatedIcon("C:\Windows\System32\certutil.exe")
BalloonTipTitle = "KeePass"
BalloonTipIcon = "Info"
Visible = $true
ContextMenu = $IconMenu
}
Together with the triggering events
$IconMenuExit.add_Click({
$form.Close()
$Notify.visible = $false
})
$form.Add_Closing(
{
$Notify.visible = $false
})
$form.Add_Resize(
{
if($form.WindowState -eq "Minimized")
{
$form.ShowInTaskbar = $false
$form.Visible = $false
$textBoxMaster.Text = ""
}
})
$IconMenuShow.add_Click({
$form.ShowInTaskbar = $true
$form.Visible = $true
$form.WindowState = "Normal"
})
Last words
I hope that someone found this article/code interesting. As I stated in the beginning. I'm not a professional programmer of any kind. Simply an hobbyist that find it very entertaining and useful in all kinds of situations.
I'll upload the code. There's more comments in the code/regions so you'll see how I organized the script. Feel free to use it in any way you want.