Private/AesCmac.psm1

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

using module ./Utilities.psm1

class AesCmac : CryptobaseUtils {
  static [int] $BlockSize = 16

  static [byte[]] ComputeTag([byte[]]$data, [byte[]]$key) {
    if ($null -eq $data) { throw [ArgumentNullException]::new("data") }
    if ($null -eq $key) { throw [ArgumentNullException]::new("key") }

    $aes = [Aes]::Create()
    $aes.Key = $key
    $aes.Mode = [CipherMode]::ECB
    $aes.Padding = [PaddingMode]::None

    $encryptor = $aes.CreateEncryptor()

    # Generate subkeys K1 and K2
    $k1 = [byte[]]::new([AesCmac]::BlockSize)
    $k2 = [byte[]]::new([AesCmac]::BlockSize)
    [AesCmac]::GenerateSubkeys($k1, $k2, $encryptor)

    $tag = [byte[]]::new([AesCmac]::BlockSize)
    [AesCmac]::ComputeCmac($tag, $data, $k1, $k2, $encryptor)

    $encryptor.Dispose()
    $aes.Dispose()

    return $tag
  }

  static [bool] VerifyTag([byte[]]$tag, [byte[]]$data, [byte[]]$key) {
    if ($null -eq $tag -or $tag.Length -ne [AesCmac]::BlockSize) { return $false }
    $computed = [AesCmac]::ComputeTag($data, $key)
    
    $diff = 0
    for ($i = 0; $i -lt [AesCmac]::BlockSize; $i++) {
      $diff = $diff -bor ($tag[$i] -bxor $computed[$i])
    }
    return $diff -eq 0
  }

  hidden static [void] GenerateSubkeys([byte[]]$k1, [byte[]]$k2, [ICryptoTransform]$aes) {
    $l = [byte[]]::new([AesCmac]::BlockSize)
    $zeros = [byte[]]::new([AesCmac]::BlockSize)
    [void]$aes.TransformBlock($zeros, 0, [AesCmac]::BlockSize, $l, 0)

    [AesCmac]::LeftShiftOneBit($k1, $l)
    if (($l[0] -band 0x80) -ne 0) {
      $k1[[AesCmac]::BlockSize - 1] = $k1[[AesCmac]::BlockSize - 1] -bxor 0x87
    }

    [AesCmac]::LeftShiftOneBit($k2, $k1)
    if (($k1[0] -band 0x80) -ne 0) {
      $k2[[AesCmac]::BlockSize - 1] = $k2[[AesCmac]::BlockSize - 1] -bxor 0x87
    }
  }

  hidden static [void] ComputeCmac([byte[]]$tag, [byte[]]$data, [byte[]]$k1, [byte[]]$k2, [ICryptoTransform]$aes) {
    $n = [int][Math]::Ceiling($data.Length / [AesCmac]::BlockSize)
    if ($n -eq 0) { $n = 1 }

    $lastBlockComplete = ($data.Length -gt 0) -and ($data.Length % [AesCmac]::BlockSize -eq 0)
    $mac = [byte[]]::new([AesCmac]::BlockSize)
    $block = [byte[]]::new([AesCmac]::BlockSize)

    for ($i = 0; $i -lt $n - 1; $i++) {
      $offset = $i * [AesCmac]::BlockSize
      [Array]::Copy($data, $offset, $block, 0, [AesCmac]::BlockSize)
      [AesCmac]::XorBlock($mac, $block)
      [void]$aes.TransformBlock($mac, 0, [AesCmac]::BlockSize, $mac, 0)
    }

    [Array]::Clear($block, 0, [AesCmac]::BlockSize)
    $lastOffset = ($n - 1) * [AesCmac]::BlockSize
    $lastLen = $data.Length - $lastOffset

    if ($lastLen -gt 0) {
      [Array]::Copy($data, $lastOffset, $block, 0, $lastLen)
    }

    if ($lastBlockComplete) {
      [AesCmac]::XorBlock($block, $k1)
    } else {
      $block[$lastLen] = 0x80
      [AesCmac]::XorBlock($block, $k2)
    }

    [AesCmac]::XorBlock($mac, $block)
    [void]$aes.TransformBlock($mac, 0, [AesCmac]::BlockSize, $mac, 0)
    [Array]::Copy($mac, $tag, [AesCmac]::BlockSize)
  }

  hidden static [void] LeftShiftOneBit([byte[]]$output, [byte[]]$input) {
    $overflow = 0
    for ($i = [AesCmac]::BlockSize - 1; $i -ge 0; $i--) {
      $output[$i] = [byte]((($input[$i] -shl 1) -bor $overflow) -band 0xFF)
      $overflow = ($input[$i] -band 0x80) ? 1 : 0
    }
  }

  hidden static [void] XorBlock([byte[]]$a, [byte[]]$b) {
    for ($i = 0; $i -lt [AesCmac]::BlockSize; $i++) {
      $a[$i] = [byte]($a[$i] -bxor $b[$i])
    }
  }
}