To je věčná otázka. Máte skript, nebo naplánovanou úlohu, nebo třeba dokonce službu, a potřebujete, aby měla k dispozici nějakou tajnou informaci, jako je třeba heslo k nějakému účtu, nebo šifrovací klíč k datům v databázi. Jak ho na počítači "bezpečně" uložíte?
Internet se hemží různými návody, jak uložit například PSCredentials do XML, nebo používat jakýsi cizí Get-PnPStoredCredential a ukládat hesla do takového toho zabudovaného trezoru na hesla.
To jsou všechno zbytečné obezličky. Já používám rovnou to nejjednodušší, přímo DPAPI, nad kterým jsou všechny ty ostatní metody stejně postaveny, a které je používáno libovolnými systémovými službami i aplikacemi. DPAPI je už minimálně od Windows 2000 zabudováno do systému a .NET Framework má pro něho už od verze 2.0 (a PowerShell 2.0 tedy také) statickou metodu, takže pohoda.
Jednoduché vysvětlení DPAPI
Detaily si můžete poslechnout na letošním hackerfest od Michaela Grafnettera. Mě zajímá princip. DPAPI (Data Protection API) vám jednoduše zašifruje cokoliv si řeknete svým náhodným klíčem. Každý počítač má svůj náhodný klíč per-machine. Tento klíč je uložen v systémových registrech počítače, nebo v systémovém profilu na disku (%windir%\system32\config\systemprofile) - Grafi bude vědět přesněji - je prostě na disku.
Každý uživatel má ve svém profilu (nejspíš opět v jeho registrech) také svůj náhodný profilový klíč, tedy per-user-profile. Schválně píšu per-user-profile, protože bez cestovního profilu máte na každém počítači jiný klíč, zatímco s profilem cestovním (roaming profile) máte všude klíč stejný. Také je důležité, že se jedná o profil, protože například IIS pracovní procesy (app pool) se ve výchozím stavu startují bez profilu a pokud u nich chcete DPAPI využívat, musíte jim profil zapnout ve vlastnostech apppool (processModel.loadUserProfile).
V uživatelském profilu je tento šifrovací náhodný klíč chráněn ještě navíc uživatelovým přihlašovacím heslem a paralelně AD záchraným klíčem. Je to tedy imunní proti offline krádeži profilu a díky AD klíči i proti resetu hesla správcem v AD. Lokální účty naopak nejsou imunní proti reset hesla správcem. Opět Grafi bude vědět detaily, předpokládám.
DPAPI umí použít buď tento per-machine, nebe per-user-profile náhodný klíč k tomu, aby vám zašifrovalo nějaká data. Pokud si to necháte zašifrovat per-machine, na daném počítači si to dešifruje úplně kdokoliv, nemusí být ani členem skupiny Administrators (příkladem je například SharePoint passphrase, jak už jsem tu o ní dávno psal). Pokud se k OS disku někdo dostane offline, také to dešifruje.
Pokud použijete klíč per-user-profile, tak to dešifruje pouze stejný uživatel na stejném počítači pokud nemá cestovní profil (roaming profile). Pokud cestovní profil má, tak na libovolném jiném počítači také.
Jak to použít z PowerShellu? Nejprve jeho obvyklé typy a práce s nimi
Člověk sbírá tajné údaje z hvězdiček jako SecureString:
$tajemstvi = Read-Host 'Zadej heslo nebo jine tajemstvi' -AsSecureString
$tajemstvi.GetType().FullName # System.Security.SecureString
Nebo si to zkonvertuje z plaintextu:
$tajemstvi = ConvertTo-SecureString 'tajemstvi co je tajne' -AsPlainText -Force
$tajemstvi.GetType().FullName # System.Security.SecureString
Nebo se zeptá rovnou na PSCredential:
$loginHeslo = Get-Credential -UserName 'gps\kamil' -Message 'Zadej heslo chlapku'
$loginHeslo.GetType().FullName # System.Management.Automation.PSCredential
PSCredential lze rovnou založit přímo z plaintextu:
$loginHeslo = New-Object System.Management.Automation.PSCredential 'gps\kamil', (ConvertTo-SecureString 'Pa$$w0rd' -AsPlainText -Force)
$loginHeslo.GetType().FullName # System.Management.Automation.PSCredential
$loginHeslo.UserName # string
$loginHeslo.Password # System.Security.SecureString
Poznámka. Už tady se používá DPAPI, sice jen paměťové, ale hodnota System.Security.SecureString je uložena v paměti opět v zašifrované formě (per-machine), aby to nešlo jenom tak prohlížet.
Ale bez problémů heslo dostanete ven i v čisté formě plaintextu:
$loginHeslo.GetNetworkCredential().Password # Pa$$w0rd
Takže práce s tajnými údaji v PowerShellu by byla. Jak to uložit na disk, nebo do registrů, nebo do databáze?
Jak si nechat zašifrovat data pomocí DPAPI?
Jednoduše. Použijeme třídu System.Security.Cryptography.ProtectedData. Žádný klíč ani heslo nepotřebujete. Všechno je v počítači nebo ve vašem profilu samo od sebe. Jediné co ještě můžete přidat, když moooc chcete, je sůl (salt), neboli inicializační vektor (IV). Akorát tím zajistíte, že dvě stejné hodnoty by nemusely být zašifrovány stejným klíčem stejně (ale ony stejně nebudou, protože DPAPI samo vždycky zasolí nějakým náhodným číslem). Spíše tím zkomplikujete kreking, řekněme. Osobně necítím potřebu, vždycky je tam systémová sůl.
$tajnyText = 'Tohle je megasuper tajne'
$bajty = [System.Text.Encoding]::Unicode.GetBytes($tajnyText)
# per-user-profile
$zasifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Protect($bajty, $null, 'CurrentUser')
[Convert]::ToBase64String($zasifrovaneBajty)
# per-machine
$zasifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Protect($bajty, $null, 'LocalMachine')
[Convert]::ToBase64String($zasifrovaneBajty)
Dobrá, slanější je vždycky chutnější:
$tajnyText = 'Tohle je megasuper tajne'
$sul = 'Hlavne to Jiriku nepresol'
$bajty = [System.Text.Encoding]::Unicode.GetBytes($tajnyText)
$iv = [System.Text.Encoding]::Unicode.GetBytes($sul)
# per-user-profile
$zasifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Protect($bajty, $iv, 'CurrentUser')
$tohleSeDaUlozit = [Convert]::ToBase64String($zasifrovaneBajty)
$tohleSeDaUlozit # proste pekny ulozitelny text ve formatu Base64
# per-machine
$zasifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Protect($bajty, $iv, 'LocalMachine')
$tohleSeDaUlozit = [Convert]::ToBase64String($zasifrovaneBajty)
$tohleSeDaUlozit # proste pekny ulozitelny text ve formatu Base64
Předpokládám, že uložit to Base64 zašifrované monstrum už zvládnete sami, ne?
Dešifrování DPAPI chráněných Base64 řetězců
Co jste si před chvilkou uložili ve formě Base64 zašifrovaného textu, si načtete a podle chuti přidáte původní inicializační vektor (IV):
$sul = 'Hlavne to Jiriku nepresol'
$iv = [System.Text.Encoding]::Unicode.GetBytes($sul)
# per-user-profile
$desifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Unprotect(([Convert]::FromBase64String($tohleSeDaUlozit)), $iv, 'CurrentUser')
[System.Text.Encoding]::Unicode.GetString($desifrovaneBajty)
# per-machine
$desifrovaneBajty = [System.Security.Cryptography.ProtectedData]::Unprotect(([Convert]::FromBase64String($tohleSeDaUlozit)), $iv, 'LocalMachine')
[System.Text.Encoding]::Unicode.GetString($desifrovaneBajty)
A jak by řekl Babica, když nemáte inicializační vektor, dáte tam něco jinýho. Akorát dejte bacha, aby vám z toho nevyšla nějaká ta jeho dobrota.