Contents of this directory is archived and no longer updated.

Posts on this page:

По просьбам трудящихся публикую скриптик, который подсчитывает хеш для файлов. Скрипт очень простой, он всего лишь выводит имя файла (полное) и значение хеша, который можно использовать как вам нужно.

function Get-FileHash {
[CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelinebyPropertyName = $true)]
        [string]$FullName,
        [ValidateSet("MD5","SHA1","SHA256","SHA384","SHA512")]
        [string]$Hash = "SHA1"
    )
    begin {
        function Test-FileLock ($file) {
            $locked = $false
            trap {
                Set-Variable -name locked -value $true -scope 1
                continue
            }
            $inputStream = New-Object system.IO.StreamReader $file
            if ($inputStream) {$inputStream.Close()}
            $locked
        }
        $Hasher = switch ($Hash) {
            "MD5" {[System.Security.Cryptography.MD5]::Create()}
            "SHA1" {[System.Security.Cryptography.SHA1]::Create()}
            "SHA256" {[System.Security.Cryptography.SHA256]::Create()}
            "SHA384" {[System.Security.Cryptography.SHA384]::Create()}
            "SHA512" {[System.Security.Cryptography.SHA512]::Create()}
        }
    }
    
    Process {
        $file = gi (Resolve-Path $FullName) -Force
        if ($file -is [IO.FileInfo]) {
            if (Test-FileLock $file) {return}
            $inputStream = New-Object System.IO.StreamReader ($file)
            $hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
            $inputStream.Close()
            $builder = New-Object System.Text.StringBuilder
            $hashBytes | %{[void]$builder.Append($_.ToString("X2"))}
            New-Object psobject -Property @{
                FullName = $file.ToString()
                Hash = $builder.ToString()
            }
        }
    }
}

Использование простое: dir c:\folder | Get-FileHash

или с указанием другого алгоритма (по умолчанию SHA1): dir c:\folder -recurse | Get-FileHash -Hash sha256

Но, всё же, для отслеживания изменённых файлов советую использовать мой PSFCIV, который специально заточен под это дело и позволяет не только считать хеши, но и проверять их.

В прошлый раз мы рассмотрели основные вопросы массивов и управления ими в Windows PowerShell. Мы теперь знаем, как они создаются, как изменять их размер (ресайзить) и какие математические операции можно проводить над массивами. В этой части я расскажу о сравнении массивов, поиске элементов и т.д.

Сравнение массивов

Как известно, в PowerShell есть куча операторов (или групп операторов) сравнения, как –eq и –like. Но здесь у нас сразу появляется проблема — эти операторы нельзя использовать для сравнения массивов. Давайте посмотрим, что получится:

[↓] [vPodans] "abc" -eq "abc"
True
[↓] [vPodans] 1,2,3 -eq 1,2,3
[↓] [vPodans]

Оператор –eq ничего не вернул. Дело в том, что справа от оператора –eq может быть только один объект, буква, строка, число и т.д. В контексте массивов оператор –eq можно использовать для получения элементов массива, содержащих конкретное значение. Например, у нас есть массив из нескольких чисел и мы хотим узнать сколько раз конкретное число использовано в массиве. Вот простой пример:

[↓] [vPodans] 2,8,5,6,2,5,4,2,7 -eq 2
2
2
2
[↓] [vPodans] 2,8,5,6,2,5,4,2,7 -eq 5
5
5
[↓] [vPodans] 2,8,5,6,2,5,4,2,7 -eq 1
[↓] [vPodans] (2,8,5,6,2,5,4,2,7 -eq 2).Length
3
[↓] [vPodans] (2,8,5,6,2,5,4,2,7 -eq 5).Length
2
[↓] [vPodans] (2,8,5,6,2,5,4,2,7 -eq 1).Length
0
[↓] [vPodans]

Из показанных примеров мы видим, что оператор –eq возвращает элементы, которые соответствуют сравниваемому объекту. Поэтому при помощи оператора –eq можно узнать, содержит ли массив конкретное значение или нет и если да, то сколько раз. То же самое относится и к оператору –like, который больше подходит для сравнения строк по маске:

[↓] [vPodans] "abc","cba","bac","bca" -like "a*"
abc
[↓] [vPodans] "abc","cba","bac","bca" -like "?b?"
abc
cba
[↓] [vPodans]

Здесь используется тот же принцип, что и с использованием оператора –eq, только с разницей, что оператор –like сравнивает по маске (нестрогое соответствие).

Для точного сравнения двух массивов следует использовать командлет Compare-Object:

[↓] [vPodans] Compare-Object 1,2,3 1,2,3
[↓] [vPodans] Compare-Object 1,2,3 1,2

                                                InputObject SideIndicator
                                                ----------- -------------
                                                          3 <=


[↓] [vPodans] Compare-Object 1,2,3 1,2,2

                                                InputObject SideIndicator
                                                ----------- -------------
                                                          2 =>
                                                          3 <=


[↓] [vPodans]

Если массивы одинаковые, командлет ничего не вернёт. Если же есть различия, вы увидите, какие элементы отсутствуют в одном массиве (направление стрелочки указывает от массива с недостающим элментом). Может быть и так, что оба массива одинаковы по размеру, но какие-то элементы имеют разные значения. Тогда вы увидите стрелочки в оба направления. Командлет Compare-Object может выводить и одинаковые элементы:

[↓] [vPodans] Compare-Object -ref 1,2,3 -dif 1,2,2 -IncludeEqual

                                                InputObject SideIndicator
                                                ----------- -------------
                                                          1 ==
                                                          2 ==
                                                          2 =>
                                                          3 <=


[↓] [vPodans]

В одинаковых элементах SideIndicator будет показывать двойной знак равенства (==).

Поиск по массиву

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

function findinarr ($array, $value) {for ($i=0; $i -lt $array.count;$i++){if($array[$i] -eq $value){$i}}}

или более развёрнутый вариант:

function findinarr ($array, $value) {
    for ($i=0; $i -lt $array.count;$i++) {
        if($array[$i] -eq $value){$i}
    }
}

Например, узнать, индекс (или индексы) элемента массива, который содержит цифру 2 и 5:

[↓] [vPodans] $a = 2,8,5,6,2,5,4,2,7
[↓] [vPodans] findinarr $a 2
0
4
7
[↓] [vPodans] findinarr $a 5
2
5
[↓] [vPodans]

Мы видим, что цифра 2 в указанном массиве содержится в элементах с индексами 0, 4 и 7, а цифра 5 в элементах с индексами 2 и 5. Не забудьте, что индексы в массивах начинаются с нуля.

Форматирование массивов

PowerShell отображает массивы в столбик, т.е. каждый элемент массива показывается на новой строке. А если вы хотите показать массив так, чтобы все элементы были в строчку, разделённой пробелами? Можно извратиться конструкцией вида:

$a = 1..100
$string = ""
$a | ForEach-Object {$string += "$_" + " "}
[↓] [vPodans] $a = 1..100
[↓] [vPodans] $string = ""
[↓] [vPodans] $a | ForEach-Object {$string += "$_" + " "}
[↓] [vPodans] $string
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 8
3 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
[↓] [vPodans]

Но можно сделать ещё круче — заключить переменную с массивом в двойные кавычки:

[↓] [vPodans] $a = 1..100
[↓] [vPodans] "$a"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 8
3 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
[↓] [vPodans]

как видите, результат точно такой же, только кода израсходовано куда меньше.

Преобразование строк в массивы и обратно

Далеко не всегда мы имеем возможность получить готовый массив, наоборот, нам нужно разбить одну строку на массив строк. Для этого можно использовать метод Split() класса System.String или оператор –split:

[↓] [vPodans] "this is a single string".split()
this
is
a
single
string
[↓] [vPodans] -split "this is a single string"
this
is
a
single
string
[↓] [vPodans]

По умолчанию, строка разбивается на массив строк по пробелу и разделителю строк. Если вы хотите разделить по другому разделителю можно его указать явно. Например, разбить MAC адрес сетевой карты на октеты:

[↓] [vPodans] "00-18-DE-54-57-8E" -split "-"
00
18
DE
54
57
8E
[↓] [vPodans]

Причём, обратите внимание, что оператор –split может быть как унарным (когда всё располагается только справа от оператора), так и бинарным (когда исходная строка находится слева от оператора, остальное располагается справа). Более подробно про оператор –split лучше всего прочитать в справке: http://technet.microsoft.com/en-us/library/dd347708.aspx. Функционал оператора достаточно интересен, но выходит за рамки рассматриваемой статьи.

Есть ещё один трюк, как разделить строку на массив символов. Для этого используется метод ToCharArray():

[↓] [vPodans] "00-18-DE-54-57-8E".tochararray()
0
0
-
1
8
-
D
E
-
5
4
-
5
7
-
8
E
[↓] [vPodans]

Есть ещё один трюк, как разбить строку на массив символов. Для этого надо строку привести к массиву символов (char[]) и результат будет точно такой же:

[char[]]"00-18-DE-54-57-8E"

Если у вас есть массив и его нужно преобразовать в одну строку, можно воспользоваться статическим методом Join() класса System.String или оператором –join:

[↓] [vPodans] $a = 1..10
[↓] [vPodans] $a
1
2
3
4
5
6
7
8
9
10
[↓] [vPodans] [string]::Join(",",$a)
1,2,3,4,5,6,7,8,9,10
[↓] [vPodans] $a -join ","
1,2,3,4,5,6,7,8,9,10
[↓] [vPodans] -join $a
12345678910
[↓] [vPodans]

Как видно из примера, оператор –join так же, как и –split бывает унарным и бинарным. Бинарная форма всегда используется, когда нужно явно указать разделитель или другие параметры оператора –join.  Если оператор унарный, он не принимает явный разделитель и просто последовательно пристыковывает элементы массива в строку. Если вам нужно преобразовать несколько массивов в строки с использованием одного и того же разделителя, в PowerShell можно использовать специальную переменную $ofs и массив явно привести к типу string:

[↓] [vPodans] $ofs = "+"
[↓] [vPodans] [string]$a
1+2+3+4+5+6+7+8+9+10
[↓] [vPodans] iex ([string]$a)
55
[↓] [vPodans]

Как можно видеть из примеров, с массивами в PowerShell можно делать что угодно (кроме вычитания и деления). Причём, зачастую, существует несколько способов выполнить одну и ту же задачу.

Реверсирование массивов

Вообще это используется не так часто, что можно было бы и опустить, но я достаточно часто работаю с CryptoAPI и бывает нужным первернуть массив. Дело в том, что CryptoAPI до мозга костей little-endian, а остальные API (даже тот же .NET) как правило big-endian. И чтобы перевернуть массив верх тормашками можно использовать статический метод Reverse() класса System.Array:

[↓] [vPodans] $a = 1..5
[↓] [vPodans] $a
1
2
3
4
5
[↓] [vPodans] [array]::Reverse($a)
[↓] [vPodans] $a
5
4
3
2
1
[↓] [vPodans]

Следует учитывать, что этот метод меняет порядок следования элементов в самой перменной и не образует выходной информации.

Эпилог

В качестве эпилога скажу, что я рассмотрел лишь самые популярные действия с массивами и это составляет лишь малую часть того, что с ними можно сделать в реальности. Но этого материала (включая предыдущую статью) вам хватит на 95% случаев. И на этом всё.

Данный пост немного не вписывается в формат моего блога, но я посчитал нужным рассказать про массивы и работу с ними в PowerShell. Мне просто нравятся массивы и всё, что с ними можно вытворять. Это очень интересно и полезно. Сразу оговорюсь, мозголомного материала здесь не будет, поэтому пост могут читать даже начинающие.

Что такое массив? Это просто набор данных. Массивы вокруг нас, ТЫСЯЧИ ИХ! Например, мой бложек — это тоже массив, массив постов. В PowerShell массивы наследуются из класса System.Array. Это означает, что любой массив будет содержать те же свойства и методы, что и упомянутый класс. Давайте посмотрим на самый простой массив и как он создаётся:

Variable = Value1, Value2, Value3 <...> ValueN

[↓] [vPodans] $a = 1,2,3
[↓] [vPodans] $a | Get-Member


   TypeName: System.Int32

Name        MemberType Definition
----        ---------- ----------
CompareTo   Method     int CompareTo(System.Object value), int CompareTo(int value)
Equals      Method     bool Equals(System.Object obj), bool Equals(int obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
GetTypeCode Method     System.TypeCode GetTypeCode()
ToString    Method     string ToString(), string ToString(string format), string ToString(System.IFormatProvider pro...


[↓] [vPodans] $a.GetType().fullname
System.Object[]
[↓] [vPodans] $a
1
2
3
[↓] [vPodans]

Мы создали простой массив состоящий из 3-х чисел: 1, 2 и 3. Однако, мы видим, что тип вовсе не System.Array, а System.Object[]. Квадратные скобки после названия класса означают, что рассматриваемый объект — массив объектов указанного класса. Например, массив объектов класса System.Object. PowerShell не использует напрямую System.Array для представления массива, а вместо этого создаёт новый объект, который наследуется из System.Array. Это означает, что массивы внутри могут содержать различную информацию и иметь любой тип (Char[], String[], Int[], ...), но набор свойств и методов будет всегда одинаковый. Чтобы убедиться в этом, посмотрите на исходный объект:

$a.PSBase | Get-Member

По умолчанию PowerShell создаёт универсальный массив, который может содержать любые данные. Доступ к элементам массива осуществляется посредством квадратных скобок следующих за переменной и индексом элемента в массиве внутри этих скобок:

[↓] [vPodans] $a[0]
1
[↓] [vPodans] $a[1]
2
[↓] [vPodans] $a[2]
3
[↓] [vPodans]

Как и во многих языках программирования, индекс элементов массива начинается с нуля (в VBS, напрмер, с 1). Вот так можно получать доступ к элементам массива. Давайте посмотрим, в чём универсальность этого массива. Сейчас он содержит только объекты типа Int32. Сейчас мы подмешаем в наш массив немного мусора:

[↓] [vPodans] $a[0] = "я - строчка!!!111одинодин"
[↓] [vPodans] $a
я - строчка!!!111одинодин
2
3
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans]

И мусор успешно добавлен. Всё дело как раз в этом объекте System.Object, который может хранить какие угодно данные. Но мы можем создать массив определённого типа. Например, массив, содержащий только числа:

[↓] [vPodans] $a = [int[]](1,2,3)
[↓] [vPodans] $a.GetType().FullName
System.Int32[]
[↓] [vPodans] $a[0] = "я - строчка!!!111одинодин"
Array assignment to [0] failed: Cannot convert value "я - строчка!!!111одинодин" to type "System.Int32". Error: "Input
string was not in a correct format.".
At line:1 char:4
+ $a[ <<<< 0] = "я - строчка!!!111одинодин"
    + CategoryInfo          : InvalidOperation: (я - строчка!!!111одинодин:String) [], RuntimeException
    + FullyQualifiedErrorId : ArrayAssignmentFailed

[↓] [vPodans]

Я создал массив, который может содержать только целые числа.  Это видно по новому типу: System.Int32[]. Поэтому если теперь мы попробуем в массив подсунуть нечто нечисловое — получим ошибку. Как ещё можно создавать массивы? Например, хочу получить массив, состоящий из одного элемента. Или не содержащий ни одного элемента. Каково, а? Для этого можно использовать вот такую форму:

@()

Значок пёсика и круглых скобок будет означать массив. Внутри скобок можно положить какие угодно данные и они будут приведены к массиву. Смотрим:

[↓] [vPodans] # пустой массив
[↓] [vPodans] $a = @()
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans] $a[0]
[↓] [vPodans] $a.Length
0
[↓] [vPodans] # массив из одного элемента
[↓] [vPodans] $a = @("я - единственный элемент в массиве")
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans] $a[0]
я - единственный элемент в массиве
[↓] [vPodans] $a.Length
1
[↓] [vPodans]

Из этого примера мы видим, что пустой массив — это тоже себе массив. Просто пустой (это видно по свойству Length, которое показывает количество элементов в массиве). Такой же формой можно привести и единичный объект к массиву из одного элемента. Этот метод создания и приведения к массиву может быть очень полезным. Например, вы обрабатываете выход некоторой команды. Этот выход может содержать один объект, а может и несколько. Это может несколько затруднить вам жизнь, потому что не у всех типов есть свойство Length, а у тех, у кого оно есть, это свойство может означать всякую всячину. Например, у System.String это свойство будет означать количество символов в строке. Именно поэтому вы можете вывод команды заключить в такую конструкцию — @() и тогда будете точно знать сколько элементов вы получили. Вот пример:

[↓] [vPodans] (Get-Item .\).length
[↓] [vPodans] @(Get-Item .\).length
1
[↓] [vPodans]

Этот пример наглядно иллюстрирует указанную проблему. Если команда вернула много элементов в массиве, вы можете использовать свойство Length, чтобы узнать сколько их там. Если команда вернула ноль или один элемент, вам свойство Length может быть недоступно и вам придётся выяснять содержимое вывода другими средствами, например, сравнивать выход команды с $null. Но, если заключить вывод команды в @(), тогда он будет гарантированно приведён к массиву и вы всегда по свойству Length узнаете сколько данных вы получили, ноль, один или больше.

Есть ещё один простенький способ создать простой массив из одного элемента — при помощи унарного оператора запятой:

[↓] [vPodans] $a = ,5
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans] $a.Length
1
[↓] [vPodans] $a = ,2,5,6
[↓] [vPodans] $a.Length
3
[↓] [vPodans]

Если запятая стоит в начале выражения, она интерпретируется как оператор и всё, что есть справа от оператора будет интерпретироваться как элементы массива. Я надеюсь, что многие знают о замечательном операторе двух точек. Этот оператор генерирует массив целых чисел (и только чисел) начиная от числа слева от оператора до числа (включительно), указанного справа от оператора:

[↓] [vPodans] $a = 5..10
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans] $a.Length
6
[↓] [vPodans]

Вот так создаётся простой массив целых чисел.

Вам кажется, что у вас массив очень маленький? У соседа он больше и он первый парень на деревне? Сделайте себе ещё больше!

Массивы можно увеличивать в размере при помощи математического оператора сложения или умножения. Уменьшать массив операторами вычитания и деления нельзя (и правильно, уменьшите свой массив и девушки на вас смотреть не будут).

[↓] [vPodans] $a = 5..10
[↓] [vPodans] $a
5
6
7
8
9
10
[↓] [vPodans] $a.Length
6
[↓] [vPodans] $a += 0
[↓] [vPodans] $a.Length
7
[↓] [vPodans] $a
5
6
7
8
9
10
0
[↓] [vPodans]

Мы сначала создали массив из 6 элементов и оператором сложения с присвоением добавили ещё один.

Примечание: команда вида $a += 0 является упрощённой записью простейшего счётчика: $a = $a + 0. То же самое и касается оператора *=. Это равносильно команде $a = $a * 0.

Массивы не резиновые (это очевидно!) и растягивать их нельзя. Внутренне PowerShell создаёт ещё один массив размером, достаточным хранить существующий массив и добавляемый. Вы этого просто не видите, а оно происходит. Доказать это можно очень просто. В самом начале поста я пытался подменить элемент массива фиксированного типа и у меня ничего хорошего из этого не вышло. А теперь попробуем вот так:

[↓] [vPodans] $a = [int[]](1,2,3)
[↓] [vPodans] $a.GetType().FullName
System.Int32[]
[↓] [vPodans] $a += "я - строчка!!!111одинодин"
[↓] [vPodans] $a
1
2
3
я - строчка!!!111одинодин
[↓] [vPodans] $a.GetType().FullName
System.Object[]
[↓] [vPodans]

Мы сначала создали массив фиксированного типа, который может хранить только целые числа. Добавив к массиву ещё один элемент, массив стал снова универсальным, большего размера и без ошибок добавил нашу строку к числовому массиву. Это следует учитывать, если вы хотите иметь массив фиксированного типа.

Для пытливых умов: чтобы создать настоящий массив фиксированного типа следует использовать класс List(T). Такой массив не будет изменять свой тип при изменении размера. И если попытаться в такой массив добавить элемент неправильного типа, вы получите ошибку.

Как я уже говорил, массивы можно не просто складывать, но и умножать, что позволит вам достаточно просто обогнать массив вашего соседа. Если при сложении вы можете складывать 2 и более массивов, то с умножением немного иначе. Вы не можете умножить один массив на другой. Умножение массива просто копирует ваш массив столько раз, сколько указано в правой части от оператора умножения:

[↓] [vPodans] $a = 1,2
[↓] [vPodans] $a * 3
1
2
1
2
1
2
[↓] [vPodans]

Мы создали простой массив из двух элементов. Умножили его на 3 и получили тот же массив, только скопипастенных 3 раза. Здесь действуют те же законы оператора умножения, что и в математике. Т.е. если умножить массив на 1, мы получим исходный массив без изменений. Как известно из школьного курса математики, любое число умноженное на 1 не изменится. И если умножить любой массив на ноль, мы молучим совершенно пустой массив:

[↓] [vPodans] $a.Length
2
[↓] [vPodans] $a *= 1
[↓] [vPodans] $a.Length
2
[↓] [vPodans] $a *= 0
[↓] [vPodans] $a.Length
0
[↓] [vPodans] $a.GetType().FullName
System.Object[]

Я посмотрел размер исходного массива и умножил его на 1. Его размер не изменился. Потом я умножил наш массив на ноль и он резко опустел. Но наш массив сам никуда не делся, он остался.

До сих пор мы рассматривали одномерные массивы. Но бывают и двумерные массивы. Они представляют собой обычную плоскую матрицу или таблицу со строками и столбцами. В мире геометрии это будет просто плоскость длиной N столбцов и высотой M строк:

[↓] [vPodans] $b = New-Object "object[,]" 3,3
[↓] [vPodans] $b[0,0] = 1
[↓] [vPodans] $b[0,1] = 2
[↓] [vPodans] $b[0,2] = 3
[↓] [vPodans] $b[1,0] = 4
[↓] [vPodans] $b[1,1] = 5
[↓] [vPodans] $b[1,2] = 6
[↓] [vPodans] $b[2,0] = 7
[↓] [vPodans] $b[2,1] = 8
[↓] [vPodans] $b[2,2] = 9
[↓] [vPodans] $b
1
2
3
4
5
6
7
8
9
[↓] [vPodans] $b[1,2]
6
[↓] [vPodans] $b[2,1]
8
[↓] [vPodans] for ($n = 0; $n -lt 3; $n++) {
>>     Write-Host $b[0,$n] $b[1,$n] $b[2,$n]
>> }
>>
1 4 7
2 5 8
3 6 9
[↓] [vPodans]

Вот таким нехитрым образом создаются двумерные массивы. Первый индекс указывает на номер столбца, второй индекс указывает номер строки. Однако, двумерные массивы нельзя складывать или умножать. В этом случае PowerShell разбивает этот массив в одномерный путём присоединения каждого последуюшего столбца к предыдущему.

На сегодня всё. В следующий раз расскажу немного о некоторых полезных операциях, которые можно проделывать с массивами, включая поиск элементов.

Подумалось, что стоит сделать итоговый пост по серии статей, в которых мы рассматривали принцип работы с p/invoke в Windows PowerShell на примере создания самоподписанных сертификатов. Здесь я оставляю ссылки на все части:

В этой части рассказывается об основных принципах работы с p/invoke, описание неуправляемых функций и структур.

Во второй части рассказывается об основных принципах и методиках работы с неуправляемой памятью и обменом данных между неуправляемой и управляемой памятью в обоих направлениях.

эта часть повествует о работе с криптопровайдерами (CSP) и процессе генерации криптографических ключей.

Заключительная часть, которая в основном рассказывает о создании расширений для сертификата и о работе с неуправляемыми массивами данных в частности.

Ссылки на другие материалы из этой серии:

В прошлый раз мы разобрали принцип получения контекста к криптопровайдеру и генерации ключевой пары. Как я уже говорил, фактически у нас всё готово, чтобы сделать сертификат, но он будет без расширений. Этим мы и займёмся в этой части. Мы добавим следующие расширения в наш сертификат:

 

Для решения этой задачи можно использовать различные API. Например, можно каждое расширение создавать средствами неуправляемых функций CryptoAPI, что значительно увеличит размер кода. А можно сделать 50/50 и какие-то вещи делать при помощи .NET и потом их заворачивать в неуправляемый код. Мы уже применяли такой ход, когда конструировали Subject сертификата. Поскольку для многих расширений (часто используемых) сертификатов есть соответствующие классы в .NET, при помощи которых мы получим значение расширений в виде байтового массива, записанного в нотации ASN.1. Сначала мы создадим пустую коллекцию расширений на основе класса X509ExtensionCollection и потом будем в неё добавлять наши расширения:

$Extensions = New-Object Security.Cryptography.X509Certificates.X509ExtensionCollection

Basic Constraints

Basic Constraints — это самое простое расширение и говорит о типе получателя сертификата — CA сервер или конечный потребитель (пользователь, служба или устройство). Для этого расширения у нас есть класс X509BasicConstraintsExtension и конструктор X509BasicConstraintsExtension(Boolean, Boolean, Int32, Boolean):

[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509BasicConstraintsExtension $false,$false,0,$false))

В таком виде у нас расширение будет для конечного потребителя (CA будет промежуточным):

[↓] [vPodans] $Extensions[0].format(1)
Subject Type=End Entity
Path Length Constraint=None

[↓] [vPodans]

Enhanced Key Usage

Довольно понятное расширение, которое отвечает за целевое назначение сертификата. Мы договорились, что будем делать сертификат для цифровой подписи. Вот его и создадим при помощи класса X509EnhancedKeyUsageExtension:

$OIDs = New-Object Security.Cryptography.OidCollection
[void]$OIDs.Add("code signing")
[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension -ArgumentList $OIDs, $false))

Key Usages

Key Usages отвечает за политику применения ключа — цифровая подпись, шифрование, обмен ключами и т.д. В нашем случае всё просто, т.к. мы делаем сертификат для цифровых подписей, следовательно, будет Signature:

[void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509KeyUsageExtension -ArgumentList "DigitalSignature", $true))

Subject Key Identifier

Это расширение содержит хеш открытого ключа рассматриваемого сертификата и может использоваться приложениями для нахождения нужного сертификата в хранилище. К сожалению, класс X509SubjectKeyIdentifierExtension не содержит удобных конструкторов для нас, поскольку ключи мы генерировали при помощи неуправляемых функций. Следовательно, чтобы применить этот класс мы или должны при помощи неуправляемых функций вытащить все данные, необходимые для создания экземпляра класса PublicKey. Или можно всю эту задачу возложить на неуправляемые функции, что будет значительно проще. Для вычисления этого расширения нам нужно:

  1. Извлечь открытый ключ в виде байтового массива при помощи функции CryptExportPublicKeyInfo;
  2. Посчитать хеш (SHA1) экспортированного открытого ключа при помощи функции CryptHashPublicKeyInfo;
  3. Записать полученное значение в нотации ASN.1 при помощи функции CryptEncodeObject.

Вот как будет выглядеть код:

# инициализируем переменную для хранения длины открытого ключа в байтах
$pcbInfo = 0
# вычисляем размер открытого ключа в байтах при помощи функции CryptExportPublicKeyInfo
if (([Quest.PowerGUI]::CryptExportPublicKeyInfo($phProv,2,1,[IntPtr]::Zero,[ref]$pcbInfo))) {
    # выделяем область памяти в неуправляемой памяти для хранения открытого ключа.
    $pbInfo = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbInfo)
    # вызываем функцию ещё раз, но при этом указываем куда экспортировать открытый ключ.
    # в данном случае мы его экспортируем в зарезервированный участок памяти.
    $Return = [Quest.PowerGUI]::CryptExportPublicKeyInfo($phProv,2,1,$pbInfo,[ref]$pcbInfo)
    # снова инициализируем переменную для хранения длины полученного хеша в байтах
    $pcbComputedHash = 0
    # вычисляем размер посчитанного хеша SHA1
    if (([Quest.PowerGUI]::CryptHashPublicKeyInfo([IntPtr]::Zero,0,0,1,$pbInfo,[IntPtr]::Zero,[ref]$pcbComputedHash))) {
        # выделяем кусок памяти для хранения этого хеша
        $pbComputedHash = [Runtime.InteropServices.Marshal]::AllocHGlobal($pcbComputedHash)
        # повторно вызываем функцию CryptHashPublicKeyInfo и указывем куда записывать
        # полученный хеш
        [void][Quest.PowerGUI]::CryptHashPublicKeyInfo([IntPtr]::Zero,0,0,1,$pbInfo,$pbComputedHash,[ref]$pcbComputedHash)
        # создаём структуру CRYPTOAPI_BLOB для описания местоположения хеша в памяти.
        # в cbData указываем размер полученного хеша, а в pbData указываем указатель
        # на область памяти, где этот хеш хранится
        $uSKI = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB -Property @{
            cbData = $pcbComputedHash;
            pbData = $pbComputedHash
        }
        # инициализируем переменную для хранения длины значения расширения, записанного в нотации ASN.1
        $pcbEncoded = 0
        # вычисляем размер значения расширения в байтах
        if (([Quest.PowerGUI]::CryptEncodeObject(1,"2.5.29.14",[ref]$uSKI,$null,[ref]$pcbEncoded))) {
            # поскольку нам нужно будет получить сам массив байтов для инициализации конструктора
            # X509SubjectKeyIdentifierExtension(AsnEncodedData, Boolean), мы не будем резервировать
            # место в неуправляемой памяти, а создадим байтовый массив в упарвляемой памяти
            $pbEncoded = New-Object byte[] -ArgumentList $pcbEncoded
            # и экспортируем наше расширение прямо в этот массив
            $Return = [Quest.PowerGUI]::CryptEncodeObject(1,"2.5.29.14",[ref]$uSKI,$pbEncoded,[ref]$pcbEncoded)
            # повторяем процедуру, чтобы создать класс AsnEncodedData. Этот класс нам нужен только для того, чтобы
            # использовать конструктор X509SubjectKeyIdentifierExtension(AsnEncodedData, Boolean). Фвктически
            # ничего пересчитываться не будет, просто мы сделаем приведение типов.
            $AsnEncodedData = New-Object Security.Cryptography.AsnEncodedData -ArgumentList "2.5.29.14", $pbEncoded
            # и вот теперь создаём управляемое расширение и добавляем его в нашу коллекцию.
            [void]$Extensions.Add((New-Object Security.Cryptography.X509Certificates.X509SubjectKeyIdentifierExtension -ArgumentList $AsnEncodedData, $false))
        }
    }
}

И вот результат работы:

[↓] [vPodans] $Extensions[1].format(0)
9f b8 3a ea 14 86 50 6c 28 f3 7b 6b 01 ed e5 91
[↓] [vPodans]

Трансформация управляемых объектов в неуправляемые структуры

Теперь у нас есть массив расширений, представленных в виде объектов .NET. Но это нам не годится, поскольку для неуправляемых функций нужно использовать неуправляемые структуры. Для начала мы каждое расширение из объектов .NET переведём в массив структур CERT_EXTENSION:

# создаём пустой массив
$uExtensionCollection = @()
# начинаем итерацию каждого расширения в массиве расширений X509ExtensionCollection
foreach ($mExt in $Extensions) {
    # создаём объект структуры CERT_EXTENSION
    $uExtension = New-Object Quest.PowerGUI+CERT_EXTENSION
    # переносим OID расширения и флаг критичности расширения
    $uExtension.pszObjId = $mExt.Oid.Value
    $uExtension.fCritical = $mExt.Critical
    # создаём структуру CRYPTOAPI_BLOB, которая будет хранить значение расширения, которое
    # у нас в виде байтового массива
    $value = New-Object Quest.PowerGUI+CRYPTOAPI_BLOB
    # сразу записываем длину значения расширения в байтах в свойство cbData
    $value.cbData = $mExt.RawData.Length
    # выделяем область памяти для хранения значения расширения
    $value.pbData = [Runtime.InteropServices.Marshal]::AllocHGlobal($value.cbData)
    # копируем байтовый массив в только что выделенную память
    [Runtime.InteropServices.Marshal]::Copy($mExt.RawData,0,$Value.pbData,$Value.cbData)
    # добавляем свойство Value структуры CERT_EXTENSION
    $uExtension.Value = $value
    # и добавляем полученную структуру в массив
    $uExtensionCollection += $uExtension
}

Вот так мы получили массив структур CERT_EXTENSION. Однако, в p/invoke и неуправляемом мире массивы структур, как и любые массивы представляются в виде нескольких последовательных блоков в памяти. Следовательно, нам нужно выполнить последовательность действий:

  • вычислить размер каждого расширения в байтах;
  • скопировать каждую структуру в память так, чтобы было использовано непрерывное адресное пространство в памяти;
  • посчитать суммарный размер всех структур в памяти и отразить это в виде структуры CERT_EXTENSIONS.

Структура CERT_EXTENSIONS очень похожа на CRYPTOAPI_BLOB и свойство cExtensions содержит количество расширений, а rgExtensions указатель на начальную точку в памяти, где хранятся эти расширения. Вот как массив управляемых объектов превращается в массив неуправляемых:

# создаём объект структуры CERT_EXTENSIONS
$uExtensions = New-Object Quest.PowerGUI+CERT_EXTENSIONS
# при помощи Marshal.SizeOf вычисляем размер каждой структуры CERT_EXTENSION и умножаем на количество
# расширений, которое у нас будет
$ExtensionSize = [Runtime.InteropServices.Marshal]::SizeOf([Quest.PowerGUI+CERT_EXTENSION]) * $Extensions.Count
$uExtensions.cExtension = $Extensions.Count
# выделяем последовательный блок памяти для хранения всех расширений
$uExtensions.rgExtension = [Runtime.InteropServices.Marshal]::AllocHGlobal($ExtensionSize)
# начинаем итерацию с каждым расширением в массиве X509ExtensionCollection
for ($n = 0; $n -lt $Extensions.Count; ++$n) {
    # вычисляем начальный адрес, в котором будет храниться расширение.
    # допустим, размер каждого расширения у нас будет 32 байта (размер структуры CERT_EXTENSION
    # значит, для первого расширения начальный адрес будет 0. Для второго расширения начальный
    # адрес будет 32, для третьего - 64 и т.д.
    $offset = $n * [Runtime.InteropServices.Marshal]::SizeOf([Quest.PowerGUI+CERT_EXTENSION])
    # поскольку у нас выделен последовательный блок памяти, мы сдвигаемся на этот размер (32 байта)
    # относительно нашего указателя памяти.
    $next = $offset + $uExtensions.rgExtension.ToInt64()
    [IntPtr]$NextAddress = New-Object IntPtr $next
    # и при помощи Marshal.StructureToPtr мы копируем неуправляемую структуру в указанный адрес
    [Runtime.InteropServices.Marshal]::StructureToPtr($uExtensionCollection[$n],$NextAddress,$false)
}

Чтобы более чётко понимать, что мы делаем, просто представьте себе ленту неограниченного размера. На этой ленте нам надо разместить несколько отрезков фиксированной длины по 32 см. При этом использовать ленту максимально эффективно. Что мы делаем? Мы выясняем количество отрезков. Зная количество отрезков и длину каждого, мы просто перемножаем эти значения и получаем суммарную длину ленты, которая нам понадобится. А потом мы просто последовательно размещаем наши отрезки к выделенной длине ленты.

Конвертирование времени

В .NET у нас есть замечательный класс DateTime, но который не поддерживается неуправляемыми функциями. Они охотнее понимают или FileTime или SystemTime. Засада с SystemTime заключается в том, что есть только одна функция, которая что-то корректно переводит в структуру SystemTimeFileTimeToSystemTime. Но нам повезло, что у DateTime есть метод DateTime.ToFileTime. Поэтому мы стандартный DateTime конвертируем в FileTime и функцией FileTimeToSystemTime преобразовываем FileTime в SystemTime. Выглядит как изврат, но более гуманного метода я не знаю. Итак, нам осталось указать начальный и конечный срок действия сертификата:

# создаём объект структуры, которая будет представлять собой начало действия сертификата
$pStartTime = New-Object Quest.PowerGUI+SystemTime
# конвертируем время объекта DateTime в SystemTime
[void][Quest.PowerGUI]::FileTimeToSystemTime([ref]$ValidFrom.ToFileTime(),[ref]$pStartTime)
# создаём объект структуры, которая будет представлять собой конец действия сертификата
$pEndTime = New-Object Quest.PowerGUI+SystemTime
# конвертируем время объекта DateTime в SystemTime
[void][Quest.PowerGUI]::FileTimeToSystemTime([ref]$ValidTo.ToFileTime(),[ref]$pEndTime)

Это очень просто.

Создание сертификата

Пока мы тут беседовали о высоких материях, мы незаметно подошли к тому, что у нас есть всё необходимое для создания самоподписанного сертификата. Вы можете в этом убедиться посмотрев на описание функции CertCreateSelfSignCertificate. И вот он, финальные 100 метров:

[Quest.PowerGUI]::CertCreateSelfSignCertificate($phProv,$ptrName,0,$PrivateKey,[IntPtr]::Zero,$pStartTime,$pEndTime,$uExtensions)
[↓] [vPodans] [Quest.PowerGUI]::CertCreateSelfSignCertificate($phProv,$ptrName,0,$PrivateKey,[IntPtr]::Zero,$pStartTime,
$pEndTime,$uExtensions)
4803696
[↓] [vPodans]

Фуцк! Эти циферки явно не похожи на сертификат. Однако, на самом деле, это очень похоже на сертификат:

The CertCreateSelfSignCertificate function builds a self-signed certificate and returns a pointer to a CERT_CONTEXT structure that represents the certificate.

У нас есть указатель на сертификат в памяти. Идём на MSDN и находим конструктор у класса X509Certificate2X509Certificate2(IntPtr) (Initializes a new instance of the X509Certificate2 class using an unmanaged handle.). Это то, что нам нужно:

[↓] [vPodans] New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696)

Thumbprint                                Subject
----------                                -------
BAE4769BDEA62AC1222A5A92A283BF356E3B78BE  CN=PowerGUI User


[↓] [vPodans] New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696) | fl *


Archived           : False
Extensions         : {System.Security.Cryptography.Oid, System.Security.Cryptography.Oid, System.Security.Cryptography.
                     Oid, System.Security.Cryptography.Oid}
FriendlyName       :
IssuerName         : System.Security.Cryptography.X509Certificates.X500DistinguishedName
NotAfter           : 09.06.2012 20:38:20
NotBefore          : 09.06.2011 20:38:13
HasPrivateKey      : True
PrivateKey         : System.Security.Cryptography.RSACryptoServiceProvider
PublicKey          : System.Security.Cryptography.X509Certificates.PublicKey
RawData            : {48, 130, 3, 7...}
SerialNumber       : 68A85F62F9626E8443D6642C2BBBAF19
SubjectName        : System.Security.Cryptography.X509Certificates.X500DistinguishedName
SignatureAlgorithm : System.Security.Cryptography.Oid
Thumbprint         : BAE4769BDEA62AC1222A5A92A283BF356E3B78BE
Version            : 3
Handle             : 4803696
Issuer             : CN=PowerGUI User
Subject            : CN=PowerGUI User



[↓] [vPodans] (New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 ([IntPtr]4803696)).Extensions |
 %{$_.format(1)}
Subject Type=End Entity
Path Length Constraint=None

Code Signing (1.3.6.1.5.5.7.3.3)

9f b8 3a ea 14 86 50 6c 28 f3 7b 6b 01 ed e5 91

Digital Signature (80)

[↓] [vPodans]

Эпик! :rock: мы его сделали! Я даже показал расширения сертификатов и вы можете убедиться, что они отвечают нашим требованиям.

Освобождение ресурсов

Но это ещё не всё. Мы использовали много разной памяти и она нам более не нужна и следует вернуть:

foreach ($uExt in $uExtensionCollection) {[void][Runtime.InteropServices.Marshal]::FreeHGlobal($uExt.Value.pbData)}
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($ptrSubject)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($uExtensions.rgExtension)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($pbInfo)
[void][Runtime.InteropServices.Marshal]::FreeHGlobal($pbComputedHash)
[void][Quest.PowerGUI]::CryptDestroyKey($phKey)
[void][Quest.PowerGUI]::CryptReleaseContext($phProv,0)

Работая с p/invoke всегда следите за тем, что если где-то выделяете ресурсы, их нужно потом высвобождать. После этого вы можете смело делать с объектом X509Certificate2 что хотите. Хотите, экспортируйте в PFX, хотите — устанавливайте в хранилище, он ваш. И напоследок, финальный скрипт (правда, без комментариев):