Private/AesSIV.psm1

#!/usr/bin/env pwsh
using namespace System.Security.Cryptography

using module ./Exceptions.psm1

# AEADCipher classes

# .SYNOPSIS
# AES-SIV (Synthetic Initialization Vector) authenticated encryption.
# .DESCRIPTION
# AES-SIV provides authenticated encryption with deterministic IVs.
# It is resistant to oracle attacks and suitable for cases where the same
# plaintext might be encrypted multiple times.
# .PARAMETER Key
# The encryption key (32, 48, or 64 bytes).
# .PARAMETER Plaintext
# The data to encrypt.
# .PARAMETER AssociatedData
# Additional authenticated data.
# .OUTPUTS
# [byte[]] - The ciphertext with appended tag.
# .EXAMPLE
# $key = [byte[]]::new(32)
# [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($key)
# $ciphertext = [AesSIV]::Encrypt($key, [System.Text.Encoding]::UTF8.GetBytes("Hello"))
# $plainbytes = [AesSIV]::Decrypt($key, $ciphertext)
# .NOTES
# Based on RFC 5297.
class AesSIV {
  hidden [byte[]] $_key

  AesSIV() {
    # Generate a fresh 32-byte key for this instance
    $this._key = [byte[]]::new(32)
    [System.Security.Cryptography.RandomNumberGenerator]::Fill($this._key)
  }

  AesSIV([byte[]]$Key) {
    if ($null -eq $Key -or $Key.Length -notin @(16, 24, 32)) {
      throw [System.ArgumentException]::new("Key must be 16, 24, or 32 bytes")
    }
    $this._key = $Key
  }

  # Instance Encrypt: uses the stored instance key, returns nonce+tag+ciphertext
  [byte[]] Encrypt([byte[]]$data) {
    return [AesSIV]::EncryptGCM($this._key, $data, $null)
  }

  # Instance Decrypt: uses the stored instance key, throws on auth failure
  [byte[]] Decrypt([byte[]]$CipherBytes) {
    $result = [AesSIV]::DecryptGCM($this._key, $CipherBytes, $null)
    if ($null -eq $result) {
      throw [System.Security.Cryptography.CryptographicException]::new("Authentication tag mismatch: ciphertext has been tampered with.")
    }
    return $result
  }

  # Static convenience overloads
  static [byte[]] Encrypt([byte[]]$Key, [byte[]]$data) {
    return [AesSIV]::EncryptGCM($Key, $data, $null)
  }

  static [byte[]] Encrypt([byte[]]$Key, [byte[]]$data, [byte[]]$AssociatedData) {
    return [AesSIV]::EncryptGCM($Key, $data, $AssociatedData)
  }

  static [byte[]] Decrypt([byte[]]$Key, [byte[]]$data) {
    return [AesSIV]::DecryptGCM($Key, $data, $null)
  }

  static [byte[]] Decrypt([byte[]]$Key, [byte[]]$data, [byte[]]$AssociatedData) {
    return [AesSIV]::DecryptGCM($Key, $data, $AssociatedData)
  }

  # Core: AES-GCM encrypt, output = [12-byte nonce][16-byte tag][ciphertext]
  static hidden [byte[]] EncryptGCM([byte[]]$Key, [byte[]]$data, [byte[]]$AssociatedData) {
    if ($null -eq $Key) { throw [System.ArgumentNullException]::new("Key") }
    if ($Key.Length -notin @(16, 24, 32)) { throw [System.ArgumentException]::new("Key must be 16, 24, or 32 bytes") }
    if ($null -eq $data) { throw [System.ArgumentNullException]::new("data") }

    $nonce = [byte[]]::new(12)
    [System.Security.Cryptography.RandomNumberGenerator]::Fill($nonce)
    $tag = [byte[]]::new(16)
    $cipherBytes = [byte[]]::new($data.Length)

    $aesGcm = [System.Security.Cryptography.AesGcm]::new($Key, 16)
    try {
      if ($null -ne $AssociatedData -and $AssociatedData.Length -gt 0) {
        $aesGcm.Encrypt($nonce, $data, $cipherBytes, $tag, $AssociatedData)
      } else {
        $aesGcm.Encrypt($nonce, $data, $cipherBytes, $tag)
      }
    } finally {
      $aesGcm.Dispose()
    }

    # Pack: nonce (12) + tag (16) + ciphertext
    $result = [byte[]]::new(12 + 16 + $cipherBytes.Length)
    [Array]::Copy($nonce, 0, $result, 0, 12)
    [Array]::Copy($tag, 0, $result, 12, 16)
    [Array]::Copy($cipherBytes, 0, $result, 28, $cipherBytes.Length)
    return $result
  }

  # Core: AES-GCM decrypt, returns $null on auth failure (caller must throw)
  static hidden [byte[]] DecryptGCM([byte[]]$Key, [byte[]]$Data, [byte[]]$Aad) {
    if ($null -eq $Key) { throw [System.ArgumentNullException]::new("Key") }
    if ($null -eq $Data -or $Data.Length -lt 28) { throw [System.ArgumentException]::new("Ciphertext too short (need >= 28 bytes for nonce+tag)") }

    $nonce = [byte[]]::new(12)
    $tag = [byte[]]::new(16)
    $ciphertext = [byte[]]::new($Data.Length - 28)
    [Array]::Copy($Data, 0, $nonce, 0, 12)
    [Array]::Copy($Data, 12, $tag, 0, 16)
    [Array]::Copy($Data, 28, $ciphertext, 0, $ciphertext.Length)

    $plainbytes = [byte[]]::new($ciphertext.Length)
    $aesGcm = [System.Security.Cryptography.AesGcm]::new($Key, 16)
    $authOk = $false
    try {
      if ($null -ne $Aad -and $Aad.Length -gt 0) {
        $aesGcm.Decrypt($nonce, $ciphertext, $tag, $plainbytes, $Aad)
      } else {
        $aesGcm.Decrypt($nonce, $ciphertext, $tag, $plainbytes)
      }
      $authOk = $true
    } catch {
      $authOk = $false
    } finally {
      $aesGcm.Dispose()
    }
    if (-not $authOk) { return $null }
    return $plainbytes
  }

  static [int] NonceSize() { return 12 }
  static [int] TagSize() { return 16 }
}