Encrypted PowerShell Cache

For a while, I've wanted to use Microsoft Graph APIs with Emacs, and I finally found the time to put all the pieces together. This is the first post where I'll cover all the various interesting things I learned and re-learend along the way.

To minimize the footprint of installation, I decided to use classic PowerShell as my main development platform. One of the important tasks is to manage OAuth access and refresh tokens to avoid continuously prompting for credentials, and to do so in a way that is secure of course.

Usage

I wanted to have a very simple data structure to keep track of a handful of things, like access tokens, refresh tokens, and expiration dates.

The basic building blocks then are a function to serialize the in-memory object to disk, encrypted, and to then decrypt and deserialize later on.

# A default object with just a verion marker
$CacheObj = @{ "ver" = 1 }
# A distinct file name for my cache, associated with the user.
$CachePath = Join-Path $env:APPDATA .graphmacs.tokencache

# Early on, read from cache
if (Test-Path $CachePath) {
  Write-Host ("Reading token cache from " + $CachePath)
  $CacheObj = Deserialize-FromEncryptedFile($CachePath)
}

# ... later, when needed, serialize it back.
Serialize-ToEncryptedFile $CacheObj $CachePath

Encryption

So, the first function is Serialize-ToEncryptedFile.

function Serialize-ToEncryptedFile($Obj, [string]$Path) {
  Add-Type -AssemblyName System.Security
  $SerializedObj = [System.Management.Automation.PSSerializer]::Serialize($Obj, [int32]::MaxValue)
  $Scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser
  $Bytes = [System.Text.Encoding]::UTF8.GetBytes($SerializedObj)
  $Protected = [System.Security.Cryptography.ProtectedData]::Protect($Bytes, $null, $Scope)
  [System.IO.File]::WriteAllBytes($Path, $Protected)
}

As an implementation detail, the serialization format is the CLI XML-based representation for objects. It's quite bloated with metatdata, but should preserve types reasonably well, and my data structure is quite small.

The other thing to note is that the encryption is done via the ProtectedData class, which is really a wrapper around DPAPI (the API for exceptionally handy per-user data protection).

Decryption

Now, once we've made all the interesting decisions at encryption time, we really have no choice but to mirror them during decryption.

function Deserialize-FromEncryptedFile([string]$Path) {
  Add-Type -AssemblyName System.Security
  $Protected = [System.IO.File]::ReadAllBytes($Path)
  $Scope = [System.Security.Cryptography.DataProtectionScope]::CurrentUser
  $Unprotected = [System.Security.Cryptography.ProtectedData]::Unprotect($Protected, $null, $Scope)
  $Text = [System.Text.Encoding]::UTF8.GetString($Unprotected)
  return [System.Management.Automation.PSSerializer]::Deserialize($Text)
}

Happy caching!

Tags:  cryptopowershelltutorial

Home