-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path23_KeyRotation.ps1
More file actions
88 lines (78 loc) · 3.55 KB
/
23_KeyRotation.ps1
File metadata and controls
88 lines (78 loc) · 3.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<#
.SYNOPSIS
OOP Reference: Key Rotation with Retention Window
.DESCRIPTION
Topic: Production key rotation — new key encrypts, old keys retained for decryption
Category: Advanced Crypto
Agent Task: Add a ReEncrypt([string]$oldKeyId, [byte[]]$ciphertext) method that
decrypts with the old key and re-encrypts with the current key.
Add Pester tests verifying a key pruned beyond retainCount cannot decrypt.
Done Conditions:
- Encrypt uses current key
- Decrypt works for any retained key
- Keys beyond retainCount throw CryptographicException on decrypt
- Pester tests pass: Invoke-Pester -Output Detailed
Non-Scope:
- No external KMS integration
#>
class RotatingKeyManager {
hidden [System.Collections.Generic.Dictionary[string,byte[]]]$_keys
hidden [System.Collections.Generic.List[string]]$_keyOrder
hidden [int]$_maxRetained
RotatingKeyManager([int]$retainCount = 3) {
$this._keys = [System.Collections.Generic.Dictionary[string,byte[]]]::new()
$this._keyOrder = [System.Collections.Generic.List[string]]::new()
$this._maxRetained = $retainCount
$this._GenerateAndActivate()
}
hidden [string] _GenerateAndActivate() {
$id = "key-$(Get-Date -Format 'yyyyMMddHHmmss')-$([System.Guid]::NewGuid().ToString('N').Substring(0,6))"
$mat = [byte[]]::new(32)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($mat)
$this._keys[$id] = $mat
$this._keyOrder.Add($id)
while ($this._keyOrder.Count -gt $this._maxRetained) {
$oldest = $this._keyOrder[0]
[System.Array]::Clear($this._keys[$oldest], 0, $this._keys[$oldest].Length)
$this._keys.Remove($oldest) | Out-Null
$this._keyOrder.RemoveAt(0)
}
return $id
}
[string] GetCurrentKeyId() { return $this._keyOrder[$this._keyOrder.Count - 1] }
[hashtable] Encrypt([byte[]]$plaintext) {
$kid = $this.GetCurrentKeyId()
$key = $this._keys[$kid]
$gcm = [System.Security.Cryptography.AesGcm]::new($key)
$n = [byte[]]::new(12); $ct = [byte[]]::new($plaintext.Length); $t = [byte[]]::new(16)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($n)
$gcm.Encrypt($n, $plaintext, $ct, $t); $gcm.Dispose()
return @{ KeyId=$kid; Ciphertext=($n + $t + $ct) }
}
[byte[]] Decrypt([string]$keyId, [byte[]]$ciphertext) {
$key = $null
if (-not $this._keys.TryGetValue($keyId, [ref]$key)) {
throw [System.Security.Cryptography.CryptographicException]"Key '$keyId' not available (expired or purged)"
}
$nonce = $ciphertext[0..11]; $tag = $ciphertext[12..27]
# null resolves to byte[] in PS 7.4.6 arm64 -- assert value
if ($ciphertext.Length -gt 28) {
$body = $ciphertext[28..($ciphertext.Length-1)]
} else {
$body = [byte[]]::new(0)
}
$pt = [byte[]]::new($body.Length)
$gcm = [System.Security.Cryptography.AesGcm]::new($key)
$gcm.Decrypt($nonce, $body, $tag, $pt); $gcm.Dispose()
return $pt
}
[void] Rotate() { $this._GenerateAndActivate() }
[string[]] GetRetainedKeyIds() { return $this._keyOrder.ToArray() }
# Agent Task: ReEncrypt method
[hashtable] ReEncrypt([string]$oldKeyId, [byte[]]$ciphertext) {
# Decrypt with old key
$plaintext = $this.Decrypt($oldKeyId, $ciphertext)
# Encrypt with current key
return $this.Encrypt($plaintext)
}
}