Contents of this directory is archived and no longer updated.

Примечание: данный пост перепечатан в связи с закрытием бложиков на spaces.live.com, как имеющий какую-то ценность для автора и/или читателей.


В сети есть уйма маленьких консольных и графических утилит, которые позволяют считать хэши MD5/SHA1/SHA256/etc для файлов. Это иногда очень полезно. Расслабляясь после ряда статей по управлению Share Permissions решил от нечего делать написать свою утилитку полностью на PowerShell, которая интегрируется с контекстным меню Windows Explorer. Ну и самописный инсталлятор на PowerShell, который всю эту интеграцию и выполнит. Конечно же, это будет выглядеть продуктом, который выполнен на коленке в поезде, но тем не менее, это неплохой пример для начала работы с GUI в PowerShell. Ну что-ж, давайте разбираться.

У нас в Windows Explorer при правом клике на файле (только файле) будет контекстное меню вот такого вида:

hash1

 

Делается это очень просто, мы просто создадим ветку в реестре и зададим ей значение следующего вида:

Ключ реестра:
HKEY_CLASSES_ROOT\*\Shell\Hash SHA1\command
Значение элемента Default:
C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe\" -nologo -noninteractive -noprofile -command get-location | hash.ps1 '%1'

Данная команда запускает PowerShell без логотипа и без профиля. Ключ -command показывает какие команды следует исполнить. В нашем случае это будет вызов скрипта hash.ps1 с аргументом в виде переменной %1 . Данная переменная будет содержать путь к файлу и использоваться как аргумент для скрипта. Как я уже рассказывал раньше про передачу аргументов из командной строки в скрипт, то при использовании пробелов в аргументах (а путь к файлу очень даже может содержать пробелы), то его нужно заключить в одинарные кавычки. Теперь давайте вспомним, как создавать путь в реестре и присваивать им занчения из PowerShell:

New-Item -Path "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" -Force

В конце строки я использовал ключ -Force, чтобы путь создался полностью, даже если реальный путь обрывается посередине. Без этого ключа PowerShell вернёт ошибку, что ключ реестра не существует и не сможет создать последующие ключи. Теперь пора внести запись в реестр. Выше я писал ключ реестра, который находится в ветке HKEY_CLASSES_ROOT, а создаю ключ в ветке HKLM (HKEY_LOCAL_MACHINE). Всё очень просто. Тут нужно понимать структуру реестра в Windows NT системах. На самом деле на диске физически хранятся лишь HKLM и множество кустов HKCU (по одному кусту на каждого пользователя), которые в итоге составляют один общий раздел HKU. Остальные же ветки реестра, которые мы видим в классическом реестре являются лишь ссылками на соответствующие ключи этих двух основных файлов кустов реестра. Для удобочитаемости кода я длинные значения присвоил переменным и в основном коде использую уже переменные:

$RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command"
$RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command C:\hash.ps1 '%1'"
New-Item -Path $RegPath -Force
New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue

После выполнения этих команд мы получим в контекстном меню элемент Hash SHA1. А теперь уже можно начинать писать основной код, который будет считать хэш файла. Мне из хэш-методов больше нравится SHA1 (кому-то может и MD5 или ещё что), поэтому писать буду применительно под него (что характерно от используемого хэш-метода код изменится только в названии класса и всё). Для SHA1 в .NET есть класс System.Security.Cryptography.SHA1. Давайте создадим новый инстанс с этим классом. Если посмотреть список членов класса SHA1, то найдём там Create. В принципе, этого хватает вполне:

$hasher = [System.Security.Cryptography.SHA1]::Create()

Хорошо, инстанс создали, что дальше? Дальше нужно подобрать метод для данного класса. Из списка методов нам для решения поставленной задачи пригодится метод ComputeHash(Stream). Stream в данном случае означает, что будет создаваться поток байтов и пропускаться через метод ComputeHash для вычисления хэша. Значит, нам нужно создать поток байтов. В описании метода ComputeHash в описании параметров указано как создавать поток байтов, а именно с использованием System.IO.Stream. Давайте создадим объект с этим классом и в качестве параметра ему передадим путь к файлу. Путь файла сожержится в переменной $file:

$inputStream = New-Object System.IO.StreamReader($file)

теперь файл будет преобразован в виде потока байтов. Ну что, теперь можно этот поток байтов можно передать в метод ComputeHash класса System.Security.Cryptography.SHA1. Сказано - сделано:

$hashBytes = $hasher.ComputeHash($inputStream.BaseStream)

Теперь завершаем поток байтов:

$inputStream.Close()

В принципе, если вместо переменной $file подставить путь к файлу (пока без вызова из контекстного меню, а просто из GUI, то мы увидим результат работы скрипта:

[C:\] $hasher = [System.Security.Cryptography.SHA1]::Create()
[C:\] $inputStream = New-Object System.IO.StreamReader ("C:\windows\Notepad.exe")
[C:\] $hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
[C:\] $inputStream.Close()
[C:\] $hashbytes
125
108
181
162
72
110
38
219
208
171
69
138
233
234
9
49
57
116
174
96

На выходе мы получили массив посчитанных байтов. Чтобы собрать этот массив в строку мы воспользуемся классом System.Text.StringBuilder. Создадим объект с этим классом:

$builder = New-Object System.Text.StringBuilder

Теперь посмотрим, какие методы у него есть - StringBuilder Members. Здесь нас заинтересует метод Append Method (Byte). Т.к. у нас массив байтов, то нужно данный метод использовать в цикле foreach-object и необходимо его привести в HEX формат:

$hashBytes | Foreach-Object {[void]$builder.Append($_.ToString("X2"))} 
$output = New-Object PsObject 
$output | Add-Member NoteProperty HashValue ([string]$builder.ToString())

В первой строке мы только сложили массив байтов в строку. Теперь нужно весь этот результат куда-то разместить. Для этого второй строкой мы создаём собственный абстрактный объект и добавляем ему свойство HashValue (можно и на своё усмотрение название придумать) и присваиваем ему значение. Давайте посмотрим, что после исполнения кода мы будем иметь в переменной $output:

[C:\] $builder = New-Object System.Text.StringBuilder
[C:\] $hashBytes | Foreach-Object {[void]$builder.Append($_.ToString("X2"))}
[C:\] $output = New-Object PsObject
[C:\] $output | Add-Member NoteProperty HashValue ([string]$builder.ToString())
[C:\] $output.hashvalue
7D6CB5A2486E26DBD0AB458AE9EA09313974AE60

Жирным я выделил содержимое переменной, а именно - итоговый хэш-код файла notepad.exe по системе SHA1! Отлично, сам конструктив уже готов (код, который для файлов считает хэш). Теперь немного GUI. Объявляем часть GUI:

[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")

В принципе, эта команда используется всегда перед использованием GUI элементов. Давайте создадим простенькую небольшую форму и зададим ей какие-нибудь свойства:

$form = new-object Windows.Forms.Form 
$form.Text = "$file SHA1 Hash" 
$form.width = 314 
$form.height = 110 
$form.startposition = "CenterScreen" 
$form.MaximizeBox = 0 
$form.MinimizeBox = 0 
$form.FormBorderStyle = "FixedSingle" 
$form.autosize = 0

Первой строкой мы создали объект Windows.Forms.Form а в последующих строках заполнил свойства этого объекта. Касательно размеров я не действовал наугад, а создал примерное окно в Visual Studio с нужными размерами. И уже из Visual Studio выбирал нужные значения для свойств объекта. Напрямую из PowerShell не очень удобно рассчитывать размеры и компоновать элементы внутри формы. Я считаю, что тут комментарии излишни по коду, т.к. значения свойств понятны из самих названий. Например, свойство Text для формы определяет заголовок формы. У меня в заголовке динамически подставляется путь из переменной $file и добавил текст SHA1 Hash. На форме у меня будет ещё два элемента - Метка (Label) и кнопка (Button). Начнём с метки:

$label = new-object Windows.Forms.label 
$label.Location = New-Object System.Drawing.Size(12,18) 
$label.autosize = 1 
$label.text = $output.hashvalue

местоположение метки относительно верхнего левого угла формы я взял из Visual Studio. В свойство Text я вписал содержимое переменной $output.hashvalue, которая динамически для каждого файла будет содержать SHA1 хэш. От созерцания хэша легче мне не станет, поэтому его нужно как-то изъять из метки простым способом (метка не поддерживает выделение и копирование текста. Даже при использовании элемента EditBox пришлось бы выделять текст и вручную копировать, что не сильно удобно). Для этого я на форме разместил кнопку, которая при нажатии будет копировать содержимое метки в буфер обмена и закрывать форму:

$button = new-object Windows.Forms.button 
$button.Location = New-Object System.Drawing.Size(12,39) 
$button.text = "Скопировать в буфер обмена" 
$button.width = 274 
$button.height = 23 
$button.add_click({
    $label.text | clip; 
    $form.close()
})

Примечение: вот тут мне пришлось отступиться от чистого кода на .NET, т.к. PowerShell не умеет нативно использовать буфер обмена и для этого нужно писать собственную функцию. В принципе, она уже есть в PowerShell Community Extensions и позволяет как записывать в буфер обмена, так и извлекать оттуда данные. У меня же нету задачи по извлечению данных из буфера обмена, поэтому я схитрил и использовал системную утилиту clip.exe, которая без расходования лишних строк позволяет скопировать данные в буфер обмена.

Вот и всё, больше ничего на моей форме не будет, поэтому можно активировать форму и объявлять в ней мои элементы:

$form.controls.add($label) 
$form.controls.add($button) 
$form.Add_Shown({$form.Activate()}) 
$form.ShowDialog()

Если создать нужные ключи реестра и сохранить этот код в файл, то при клике на контекстное меню Hash SHA1 появится вот такое окошко:

image

 

Конечно же тут есть один недостаток - на заднем плане нашей формы будет маячить чёрная консоль PowerShell. К сожалению я пока не придумал, как сделать по-тихому, чтобы на экране была бы только форма без окна консоли. Если придумаю, то обязательно напишу или, если у вас есть уже готовые наработки или идеи, оставьте их в комментариях. Теперь настало время подвести итог и написать конечный PS1 файл:

######################################################## 
# Hash SHA1_install.ps1 
# Version 1.0 
# 
# SHA1 hash calculator for a file from context menu 
# 
# Vadims Podans (c) 2008 
# http://vpodans.spaces.live.com/ 
######################################################## 

$FilePath = Read-Host "Укажите путь для размещения Hash.PS1" 
if (Test-Path $FilePath) { 
    $FilePath = $FilePath + "\" + "hash.ps1" 
    $RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" 
    $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command $FilePath '%1'" 
    New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue 
    if (Test-Path -LiteralPath $RegPath) { 
        New-ItemProperty -LiteralPath $RegPath -Name "(Default)" -Value $RegValue 
        New-Item -ItemType file -Path $FilePath -Force -ErrorAction SilentlyContinue 
        if (Test-Path $FilePath) {
            $exefile = 'param ($file) 
            $hasher = [System.Security.Cryptography.SHA1]::Create() 
            $inputStream = New-Object System.IO.StreamReader ($file) 
            $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) 
            $inputStream.Close() 
            $builder = New-Object System.Text.StringBuilder 
            $hashBytes | Foreach-Object { [void] $builder.Append($_.ToString("X2")) } 
            $output = New-Object PsObject 
            $output | Add-Member NoteProperty HashValue ([string]$builder.ToString()) 

            # creating a form 

            [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms") 
            $form = new-object Windows.Forms.Form 
            $form.Text = "$file SHA1 Hash" 
            $form.width = 314 
            $form.height = 110 
            $form.startposition = "CenterScreen" 
            $form.MaximizeBox = 0 
            $form.MinimizeBox = 0 
            $form.FormBorderStyle = "FixedSingle" 
            $form.autosize = 0 

            # add a label 

            $label = new-object Windows.Forms.label 
            $label.Location = New-Object System.Drawing.Size(12,18) 
            $label.autosize = 1 
            $label.text = $output.hashvalue 

            # add a button 

            $button = new-object Windows.Forms.button 
            $button.Location = New-Object System.Drawing.Size(12,39) 
            $button.text = "Скопировать в буфер обмена" 
            $button.width = 274 
            $button.height = 23 
            $button.add_click({$label.text | clip; 
            $form.close()}) 

            # showing the form 

            $form.controls.add($label) 
            $form.controls.add($button) 
            $form.Add_Shown({$form.Activate()}) 
            $form.ShowDialog()'
            Set-Content -Path $FilePath -Value $exefile
        } else {Write-Warning "Не удалось создать файл по пути: $FilePath. Операция прервана."}
    } else {Write-Warning "Не удалось создать ключ реестра. Возможно у вас нету прав на запись в раздел HKLM. Операция прервана."}
}

В конечном итоге я обвязал код некоторыми проверками, которые проверяют возможность записи в реестр (для работы скрипта нужно иметь право записи в HKLM) и в указанную пользователем папку. Если какое-либо действие не удастся, то код выдаст соответствующее сообщение об ошибке и прервёт дальнейшую операцию. Если обе операции записи прошли успешно, то происходит копирование рабочего кода (содержимое переменной $exefile), который уже не содержит инсталляционных и проверочных строк кода. Вот как бы и всё на сегодня :-) .


Share this article:

Comments:

Comments are closed.