Private/XSalsa20.psm1

#!/usr/bin/env pwsh
using namespace System

using module ./Utilities.psm1

class XSalsa20 : CryptobaseUtils {
  static [uint[]] $Constants = @([uint]0x61707865, [uint]0x3320646e, [uint]0x79622d32, [uint]0x6b206574)
  static [int] $KEY_SIZE = 32
  static [int] $NONCE_SIZE = 24
  static [int] $BLOCK_SIZE = 64

  static [byte[]] Encrypt([byte[]]$inputbytes, [byte[]]$key, [byte[]]$nonce) { return [XSalsa20]::Transform($inputbytes, $key, $nonce, 0) }
  static [byte[]] Encrypt([byte[]]$inputbytes, [byte[]]$key, [byte[]]$nonce, [uint]$counter = 0) { return [XSalsa20]::Transform($inputbytes, $key, $nonce, $counter) }
  static [byte[]] Decrypt([byte[]]$inputbytes, [byte[]]$key, [byte[]]$nonce) { return [XSalsa20]::Transform($inputbytes, $key, $nonce, 0) }
  static [byte[]] Decrypt([byte[]]$inputbytes, [byte[]]$key, [byte[]]$nonce, [uint]$counter = 0) { return [XSalsa20]::Transform($inputbytes, $key, $nonce, $counter) }

  static [byte[]] Transform([byte[]]$inputbytes, [byte[]]$key, [byte[]]$nonce, [uint]$counter = 0) {
    if ($null -eq $key -or $key.Length -ne [XSalsa20]::KEY_SIZE) { throw [ArgumentException]::new("Key must be 32 bytes.") }
    if ($null -eq $nonce -or $nonce.Length -ne [XSalsa20]::NONCE_SIZE) { throw [ArgumentException]::new("Nonce must be 24 bytes.") }

    $derivedKey = [byte[]]::new(32)
    $derivedNonce = [byte[]]::new(8)
    [XSalsa20]::DeriveKeyAndNonce($derivedKey, $derivedNonce, $key, $nonce)

    $output = [byte[]]::new($inputbytes.Length)
    $state = [uint[]]::new(16)
    $keystream = [byte[]]::new([XSalsa20]::BLOCK_SIZE)

    $blocks = [int][Math]::Ceiling($inputbytes.Length / [XSalsa20]::BLOCK_SIZE)
    for ($b = 0; $b -lt $blocks; $b++) {
      $effectiveCounter = $counter + [uint]$b
      $start = $b * [XSalsa20]::BLOCK_SIZE
      $len = [Math]::Min([XSalsa20]::BLOCK_SIZE, $inputbytes.Length - $start)

      [XSalsa20]::InitializeSalsa20State($state, $derivedKey, $derivedNonce, $effectiveCounter)
      [XSalsa20]::GenerateKeystreamBlock($keystream, $state)

      for ($i = 0; $i -lt $len; $i++) {
        $output[$start + $i] = [byte]($inputbytes[$start + $i] -bxor $keystream[$i])
      }
    }

    return $output
  }

  hidden static [void] DeriveKeyAndNonce([byte[]]$derivedKey, [byte[]]$derivedNonce, [byte[]]$key, [byte[]]$nonce) {
    $hsalsaNonce = [byte[]]::new(16)
    [Array]::Copy($nonce, 0, $hsalsaNonce, 0, 16)
    [XSalsa20]::HSalsa20($derivedKey, $key, $hsalsaNonce)
    [Array]::Copy($nonce, 16, $derivedNonce, 0, 8)
  }

  hidden static [void] HSalsa20([byte[]]$output, [byte[]]$key, [byte[]]$nonce) {
    $state = [uint[]]::new(16)
    $state[0] = [XSalsa20]::Constants[0]
    $state[1] = [XSalsa20]::Constants[1]
    $state[2] = [XSalsa20]::Constants[2]
    $state[3] = [XSalsa20]::Constants[3]
    for ($i = 0; $i -lt 8; $i++) { $state[4 + $i] = [XSalsa20]::ReadUInt32LE($key, $i * 4) }
    for ($i = 0; $i -lt 4; $i++) { $state[12 + $i] = [XSalsa20]::ReadUInt32LE($nonce, $i * 4) }

    for ($i = 0; $i -lt 10; $i++) {
      [XSalsa20]::QuarterRound($state, 0, 4, 8, 12)
      [XSalsa20]::QuarterRound($state, 5, 9, 13, 1)
      [XSalsa20]::QuarterRound($state, 10, 14, 2, 6)
      [XSalsa20]::QuarterRound($state, 15, 3, 7, 11)
      [XSalsa20]::QuarterRound($state, 0, 1, 2, 3)
      [XSalsa20]::QuarterRound($state, 5, 6, 7, 4)
      [XSalsa20]::QuarterRound($state, 10, 11, 8, 9)
      [XSalsa20]::QuarterRound($state, 15, 12, 13, 14)
    }

    [XSalsa20]::WriteUInt32LE($output, 0, $state[0])
    [XSalsa20]::WriteUInt32LE($output, 4, $state[5])
    [XSalsa20]::WriteUInt32LE($output, 8, $state[10])
    [XSalsa20]::WriteUInt32LE($output, 12, $state[15])
    [XSalsa20]::WriteUInt32LE($output, 16, $state[6])
    [XSalsa20]::WriteUInt32LE($output, 20, $state[7])
    [XSalsa20]::WriteUInt32LE($output, 24, $state[8])
    [XSalsa20]::WriteUInt32LE($output, 28, $state[9])
  }

  hidden static [void] InitializeSalsa20State([uint[]]$state, [byte[]]$key, [byte[]]$nonce, [uint]$counter) {
    $state[0] = [XSalsa20]::Constants[0]
    $state[1] = [XSalsa20]::Constants[1]
    $state[2] = [XSalsa20]::Constants[2]
    $state[3] = [XSalsa20]::Constants[3]
    for ($i = 0; $i -lt 4; $i++) { $state[4 + $i] = [XSalsa20]::ReadUInt32LE($key, $i * 4) }
    $state[8] = $counter
    $state[9] = 0
    $state[10] = [XSalsa20]::ReadUInt32LE($nonce, 0)
    $state[11] = [XSalsa20]::ReadUInt32LE($nonce, 4)
    for ($i = 0; $i -lt 4; $i++) { $state[12 + $i] = [XSalsa20]::ReadUInt32LE($key, ($i + 4) * 4) }
  }

  hidden static [void] GenerateKeystreamBlock([byte[]]$keystream, [uint[]]$state) {
    $working = [uint[]]::new(16)
    [Array]::Copy($state, $working, 16)

    for ($i = 0; $i -lt 10; $i++) {
      [XSalsa20]::QuarterRound($working, 0, 4, 8, 12)
      [XSalsa20]::QuarterRound($working, 5, 9, 13, 1)
      [XSalsa20]::QuarterRound($working, 10, 14, 2, 6)
      [XSalsa20]::QuarterRound($working, 15, 3, 7, 11)
      [XSalsa20]::QuarterRound($working, 0, 1, 2, 3)
      [XSalsa20]::QuarterRound($working, 5, 6, 7, 4)
      [XSalsa20]::QuarterRound($working, 10, 11, 8, 9)
      [XSalsa20]::QuarterRound($working, 15, 12, 13, 14)
    }

    for ($i = 0; $i -lt 16; $i++) {
      [XSalsa20]::WriteUInt32LE($keystream, $i * 4, [uint](([uint64]$working[$i] + $state[$i]) -band 0xFFFFFFFFuL))
    }
  }

  hidden static [void] QuarterRound([uint[]]$state, [int]$a, [int]$b, [int]$c, [int]$d) {
    $state[$b] = $state[$b] -bxor [XSalsa20]::RotateLeft([uint](([uint64]$state[$a] + $state[$d]) -band 0xFFFFFFFFuL), 7)
    $state[$c] = $state[$c] -bxor [XSalsa20]::RotateLeft([uint](([uint64]$state[$b] + $state[$a]) -band 0xFFFFFFFFuL), 9)
    $state[$d] = $state[$d] -bxor [XSalsa20]::RotateLeft([uint](([uint64]$state[$c] + $state[$b]) -band 0xFFFFFFFFuL), 13)
    $state[$a] = $state[$a] -bxor [XSalsa20]::RotateLeft([uint](([uint64]$state[$d] + $state[$c]) -band 0xFFFFFFFFuL), 18)
  }

  hidden static [uint] RotateLeft([uint]$value, [int]$bits) {
    return ($value -shl $bits) -bor ($value -shr (32 - $bits))
  }

  hidden static [uint] ReadUInt32LE([byte[]]$buf, [int]$off) {
    return [uint](([uint]$buf[$off]) -bor ([uint]$buf[$off + 1] -shl 8) -bor ([uint]$buf[$off + 2] -shl 16) -bor ([uint]$buf[$off + 3] -shl 24))
  }

  hidden static [void] WriteUInt32LE([byte[]]$buf, [int]$off, [uint]$val) {
    $buf[$off] = [byte]($val -band 0xFF)
    $buf[$off + 1] = [byte](($val -shr 8) -band 0xFF)
    $buf[$off + 2] = [byte](($val -shr 16) -band 0xFF)
    $buf[$off + 3] = [byte](($val -shr 24) -band 0xFF)
  }
}