From 817c981416846e4bd3ba089edf6f6782b8822b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Hoffmann=20S=C3=B8rensen?= Date: Thu, 4 Jun 2026 16:41:20 +0200 Subject: [PATCH 1/2] Fixed saml decryptor tool --- Tools.SAMLResponseDecryptor/Program.cs | 184 ++++++++++++++---- Tools.SAMLResponseDecryptor/SAMLDecryptor.cs | 137 +++++++++---- .../Tools.SAMLResponseDecryptor.csproj | 3 + 3 files changed, 252 insertions(+), 72 deletions(-) diff --git a/Tools.SAMLResponseDecryptor/Program.cs b/Tools.SAMLResponseDecryptor/Program.cs index 2a173a195f..eab8d98d6c 100644 --- a/Tools.SAMLResponseDecryptor/Program.cs +++ b/Tools.SAMLResponseDecryptor/Program.cs @@ -2,6 +2,7 @@ using System.IO; using System.Text; using System.Xml; +using Spectre.Console; namespace Tools.SAMLResponseDecryptor { @@ -9,58 +10,170 @@ internal class Program { private static void Main() { - Console.WriteLine(@"Enter the path for the certificate file (eg C:\Temp\MyCertificate.pfx, __no quotes__):"); - var certificatePath = Console.ReadLine(); - if (!File.Exists(certificatePath)) + AnsiConsole.Write(new Rule("[bold deepskyblue1]SAML Response Decryptor[/]").RuleStyle("deepskyblue1")); + AnsiConsole.WriteLine(); + + string certPath; + while (true) { - Console.WriteLine("Certificate file not found."); - return; + certPath = AskPath("[deepskyblue1]Certificate file[/] [grey](PFX path, no quotes):[/]"); + if (File.Exists(certPath)) break; + AnsiConsole.MarkupLine("[red]File not found, try again.[/]"); } - Console.WriteLine("Enter the password for the certificate (from 1pwd):"); - var certificatePassword = Console.ReadLine() ?? string.Empty; - Console.WriteLine("Enter the SAML response symmetric key ():"); - var key = ReadValueOrFile(); - var decryptedSymmetricKey = SAMLDecryptor.DecryptSymmetricKey(key, certificatePath, certificatePassword); + var certPassword = AnsiConsole.Prompt( + new TextPrompt("[deepskyblue1]Certificate password[/] [grey](from 1Password):[/]") + .Secret()); - Console.WriteLine("Enter the SAML response assertion (), or a file path prefixed with 'file:':"); - Console.WriteLine(" (Tip: save the value to a .txt file and enter: file:C:\\Temp\\assertion.txt)"); - var data = ReadValueOrFile(); + AnsiConsole.WriteLine(); + AnsiConsole.Write( + new Panel( + "1. Open [bold]DevTools → Network[/] tab in your browser [grey](F12)[/] and start a new recording\n" + + "2. Trigger the KOMBIT SSO login flow for the KITOS application\n" + + "3. Locate the [bold]POST[/] request to [bold]Login.ashx[/] [grey](this is the SSO callback sent by KOMBIT/n2adgangsstyring back to your SP)[/]\n" + + "4. Open the [bold]Payload[/] tab and copy the [bold]SAMLResponse[/] form field value\n" + + "5. URL-decode the value, then Base64-decode it — save the resulting XML as a [bold].xml[/] file") + .Header("[bold yellow] How to capture the SAML response [/]") + .BorderColor(Color.Yellow) + .Padding(new Padding(1, 0, 1, 0))); + AnsiConsole.WriteLine(); - Console.WriteLine($"[DEBUG] Symmetric key : {decryptedSymmetricKey.Length} bytes ({decryptedSymmetricKey.Length * 8} bits)"); - Console.WriteLine($"[DEBUG] Assertion data : {Convert.FromBase64String(data).Length} bytes"); + var samlFilePath = AskPath("[deepskyblue1]SAML response XML file[/] [grey](path, or Enter for manual input):[/]"); - var decryptedAssertion = SAMLDecryptor.DecryptAssertion(data, decryptedSymmetricKey); - Console.WriteLine("---"); - Console.WriteLine($"Decrypted assertion: {decryptedAssertion}"); - Console.WriteLine("---"); - Console.WriteLine("Extracting privilege..."); - var xml = new XmlDocument(); - xml.LoadXml(decryptedAssertion); - var nsmgr = new XmlNamespaceManager(xml.NameTable); - nsmgr.AddNamespace("samlp", "urn:oasis:names:tc:SAML:2.0:protocol"); + string key, data, assertionAlgorithm = string.Empty; + + if (!string.IsNullOrEmpty(samlFilePath)) + { + if (!File.Exists(samlFilePath)) + { + AnsiConsole.MarkupLine($"[red]File not found:[/] {Markup.Escape(samlFilePath)}"); + return; + } + if (!ExtractCipherValuesFromSamlFile(samlFilePath, out key, out data, out assertionAlgorithm)) + return; + } + else + { + key = AskValue("[deepskyblue1]Encrypted symmetric key[/] [grey](, or file:path):[/]"); + data = AskValue("[deepskyblue1]Assertion cipher value[/] [grey](, or file:path):[/]"); + } + + byte[] decryptedSymmetricKey; + try + { + decryptedSymmetricKey = SAMLDecryptor.DecryptSymmetricKey(key, certPath, certPassword); + } + catch (System.Security.Cryptography.CryptographicException ex) + { + AnsiConsole.MarkupLine($"[red]Could not decrypt symmetric key:[/] {Markup.Escape(ex.Message)}"); + AnsiConsole.MarkupLine("[grey]Possible causes: wrong certificate, invalid PFX password, or all RSA padding modes failed.[/]"); + return; + } + + string decryptedAssertion; + try + { + decryptedAssertion = SAMLDecryptor.DecryptAssertion(data, decryptedSymmetricKey, + string.IsNullOrEmpty(assertionAlgorithm) ? null : assertionAlgorithm); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Could not decrypt assertion:[/] {Markup.Escape(ex.Message)}"); + return; + } + + AnsiConsole.WriteLine(); + AnsiConsole.Write(new Rule("[bold green]Decrypted Assertion[/]").RuleStyle("green")); + AnsiConsole.WriteLine(decryptedAssertion); + AnsiConsole.Write(new Rule().RuleStyle("green")); + AnsiConsole.WriteLine(); + + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(decryptedAssertion); + var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable); nsmgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion"); - nsmgr.AddNamespace("xenc", "http://www.w3.org/2001/04/xmlenc#"); - nsmgr.AddNamespace("ds", "http://www.w3.org/2000/09/xmldsig#"); - var node = xml.SelectSingleNode("//saml:Attribute[@Name='dk:gov:saml:attribute:Privileges_intermediate']/saml:AttributeValue", nsmgr); + + var node = xmlDoc.SelectSingleNode( + "//saml:Attribute[@Name='dk:gov:saml:attribute:Privileges_intermediate']/saml:AttributeValue", + nsmgr); + if (node == null) { - Console.WriteLine("No privileges found"); + AnsiConsole.MarkupLine("[yellow]No Privileges_intermediate attribute found in the assertion.[/]"); } else { - var attributeValue = node.InnerText; - var baseDecodedPrivilege = Convert.FromBase64String(attributeValue); - var decodedPrivilege = Encoding.UTF8.GetString(baseDecodedPrivilege); - Console.WriteLine($"Privilege={decodedPrivilege}"); + var decodedPrivilege = Encoding.UTF8.GetString(Convert.FromBase64String(node.InnerText)); + AnsiConsole.Write(new Rule("[bold green]Privileges[/]").RuleStyle("green")); + AnsiConsole.WriteLine(decodedPrivilege); + AnsiConsole.Write(new Rule().RuleStyle("green")); } + + AnsiConsole.WriteLine(); + AnsiConsole.Markup("[grey]Press Enter to exit.[/] "); Console.ReadLine(); } - private static string ReadValueOrFile() + private static bool ExtractCipherValuesFromSamlFile(string samlFilePath, + out string encryptedKeyCipherValue, out string assertionCipherValue, out string assertionEncryptionAlgorithm) + { + encryptedKeyCipherValue = string.Empty; + assertionCipherValue = string.Empty; + assertionEncryptionAlgorithm = string.Empty; + + var xml = new XmlDocument(); + xml.Load(samlFilePath); + + var ns = new XmlNamespaceManager(xml.NameTable); + ns.AddNamespace("xenc", "http://www.w3.org/2001/04/xmlenc#"); + + // First is inside — the RSA-encrypted symmetric key. + // Second is inside — the AES-encrypted assertion. + var encMethodNode = xml.SelectSingleNode("//xenc:EncryptedData/xenc:EncryptionMethod", ns); + assertionEncryptionAlgorithm = encMethodNode?.Attributes?["Algorithm"]?.Value ?? string.Empty; + + var cipherValues = xml.SelectNodes("//xenc:CipherData/xenc:CipherValue", ns); + if (cipherValues == null || cipherValues.Count < 2) + { + AnsiConsole.MarkupLine($"[red]Expected 2 CipherValue nodes, found {cipherValues?.Count ?? 0}.[/]"); + AnsiConsole.MarkupLine("[grey]Ensure the file contains a full EncryptedAssertion with both EncryptedKey and EncryptedData.[/]"); + return false; + } + + encryptedKeyCipherValue = cipherValues[0]!.InnerText.Trim(); + assertionCipherValue = cipherValues[1]!.InnerText.Trim(); + + var algoShort = assertionEncryptionAlgorithm.Contains("gcm", StringComparison.OrdinalIgnoreCase) ? "AES-256-GCM" : + assertionEncryptionAlgorithm.Contains("cbc", StringComparison.OrdinalIgnoreCase) ? "AES-256-CBC" : + assertionEncryptionAlgorithm; + + AnsiConsole.MarkupLine( + $"[grey]Algorithm:[/] [white]{Markup.Escape(algoShort)}[/] " + + $"[grey]key:[/] {encryptedKeyCipherValue.Length} chars " + + $"[grey]assertion:[/] {assertionCipherValue.Length} chars"); + return true; + } + + /// Prints a coloured label and reads the raw typed path (no file-content resolution). + private static string AskPath(string markupLabel) + { + AnsiConsole.Markup(markupLabel + " "); + return Console.ReadLine()?.Trim() ?? string.Empty; + } + + /// + /// Prints a coloured label and reads a value that may optionally be a file:-prefixed path + /// or a bare file path, in which case the file's text contents are returned instead. + /// + private static string AskValue(string markupLabel) { + AnsiConsole.Markup(markupLabel + " "); var input = Console.ReadLine()?.Trim() ?? string.Empty; + return ResolveFile(input); + } + private static string ResolveFile(string input) + { if (input.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) { var filePath = input["file:".Length..].Trim(); @@ -68,13 +181,8 @@ private static string ReadValueOrFile() throw new FileNotFoundException($"File not found: {filePath}"); return File.ReadAllText(filePath).Trim(); } - if (File.Exists(input)) - { - Console.WriteLine($"[INFO] Reading value from file: {input}"); return File.ReadAllText(input).Trim(); - } - return input; } } diff --git a/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs b/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs index ab196ef381..ab366100ab 100644 --- a/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs +++ b/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs @@ -2,69 +2,138 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using Spectre.Console; public class SAMLDecryptor { public static byte[] DecryptSymmetricKey(string encryptedKeyBase64, string privateKeyPath, string certificatePassword) { - // X509CertificateLoader is the non-obsolete replacement for the X509Certificate2 constructor (SYSLIB0057) - var certificate = X509CertificateLoader.LoadPkcs12FromFile( - privateKeyPath, - certificatePassword, - X509KeyStorageFlags.Exportable); - - // GetRSAPrivateKey() returns null if the certificate has no RSA private key (CS8602) - var rsa = certificate.GetRSAPrivateKey() - ?? throw new CryptographicException("Certificate does not contain an RSA private key."); - - byte[] encryptedKeyBytes = Convert.FromBase64String(encryptedKeyBase64); + var keyStorageAttempts = new[] + { + (X509KeyStorageFlags.EphemeralKeySet, "EphemeralKeySet"), + (X509KeyStorageFlags.DefaultKeySet, "DefaultKeySet"), + (X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable, "MachineKeySet+Exportable"), + }; var paddingModes = new[] { - (RSAEncryptionPadding.OaepSHA256, "OAEP SHA-256"), (RSAEncryptionPadding.OaepSHA1, "OAEP SHA-1"), - (RSAEncryptionPadding.Pkcs1, "PKCS#1 v1.5"), + (RSAEncryptionPadding.OaepSHA256, "OAEP SHA-256"), + (RSAEncryptionPadding.Pkcs1, "PKCS#1 v1.5"), }; - foreach (var (padding, name) in paddingModes) + byte[] encryptedKeyBytes = Convert.FromBase64String(encryptedKeyBase64); + + foreach (var (storageFlags, _) in keyStorageAttempts) { + X509Certificate2 certificate; try { - byte[] symmetricKey = rsa.Decrypt(encryptedKeyBytes, padding); - Console.WriteLine($"[OK] Decrypted with padding: {name}"); - return symmetricKey; + // X509CertificateLoader is the non-obsolete replacement for the X509Certificate2 constructor (SYSLIB0057) + certificate = X509CertificateLoader.LoadPkcs12FromFile(privateKeyPath, certificatePassword, storageFlags); } catch (CryptographicException) { - Console.WriteLine($"[FAIL] Padding {name} did not work."); + continue; + } + + var rsa = certificate.GetRSAPrivateKey(); + if (rsa is null) + continue; + + int rsaKeySizeBytes = rsa.KeySize / 8; + if (rsaKeySizeBytes != encryptedKeyBytes.Length) + AnsiConsole.MarkupLine( + $"[yellow]Warning:[/] RSA key is {rsaKeySizeBytes} bytes but encrypted key is {encryptedKeyBytes.Length} bytes — decryption may fail."); + + foreach (var (padding, paddingName) in paddingModes) + { + try + { + byte[] symmetricKey = rsa.Decrypt(encryptedKeyBytes, padding); + + var table = new Table().NoBorder().HideHeaders(); + table.AddColumn("k").AddColumn("v"); + table.AddRow("[grey]Subject[/]", Markup.Escape(certificate.Subject)); + table.AddRow("[grey]Serial[/]", certificate.SerialNumber); + table.AddRow("[grey]Key[/]", $"{rsa.KeySize} bits · {paddingName}"); + AnsiConsole.Write(table); + AnsiConsole.MarkupLine( + $"[green]Symmetric key decrypted[/] [grey]({symmetricKey.Length * 8}-bit)[/]"); + + return symmetricKey; + } + catch (CryptographicException) + { + // try next padding + } } } - throw new CryptographicException("Failed to decrypt the symmetric key with any known RSA padding mode."); + throw new CryptographicException( + "Failed to decrypt the symmetric key with any combination of key storage flags and RSA padding mode."); } - public static string DecryptAssertion(string encryptedAssertionBase64, byte[] symmetricKey) + public static string DecryptAssertion(string encryptedAssertionBase64, byte[] symmetricKey, string? encryptionAlgorithm = null) { byte[] encryptedAssertionBytes = Convert.FromBase64String(encryptedAssertionBase64); - // AES-256-GCM (xmlenc11): layout is [nonce:12][ciphertext:n][tag:16] - const int nonceSize = 12; // GCM nonce: 96 bits - const int tagSize = 16; // GCM auth tag: 128 bits + const string gcmUri = "http://www.w3.org/2009/xmlenc11#aes256-gcm"; - if (encryptedAssertionBytes.Length < nonceSize + tagSize) - throw new CryptographicException( - $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a GCM nonce and tag."); + bool useGcm = string.Equals(encryptionAlgorithm, gcmUri, StringComparison.OrdinalIgnoreCase) + || string.IsNullOrEmpty(encryptionAlgorithm); // auto-detect: try GCM first - byte[] nonce = encryptedAssertionBytes[..nonceSize]; - byte[] tag = encryptedAssertionBytes[^tagSize..]; - byte[] cipherText = encryptedAssertionBytes[nonceSize..^tagSize]; - byte[] plainText = new byte[cipherText.Length]; + if (useGcm) + { + // AES-256-GCM (xmlenc11#aes256-gcm): layout is [nonce:12][ciphertext:n][tag:16] + const int nonceSize = 12; + const int tagSize = 16; - Console.WriteLine($"[DEBUG] GCM nonce={nonceSize}B, ciphertext={cipherText.Length}B, tag={tagSize}B"); + if (encryptedAssertionBytes.Length >= nonceSize + tagSize) + { + try + { + byte[] nonce = encryptedAssertionBytes[..nonceSize]; + byte[] cipherText = encryptedAssertionBytes[nonceSize..^tagSize]; + byte[] tag = encryptedAssertionBytes[^tagSize..]; - using var aesGcm = new AesGcm(symmetricKey, tagSize); - aesGcm.Decrypt(nonce, cipherText, tag, plainText); + using var aesGcm = new AesGcm(symmetricKey, tagSizeInBytes: tagSize); + byte[] plainText = new byte[cipherText.Length]; + aesGcm.Decrypt(nonce, cipherText, tag, plainText); + return Encoding.UTF8.GetString(plainText); + } + catch (CryptographicException) when (string.IsNullOrEmpty(encryptionAlgorithm)) + { + // GCM failed during auto-detection — fall through to CBC + } + } + + // If algorithm was explicitly GCM but data is too short, give a clear error + if (!string.IsNullOrEmpty(encryptionAlgorithm)) + throw new CryptographicException( + $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a GCM nonce and tag."); + } - return Encoding.UTF8.GetString(plainText); + { + // AES-256-CBC (xmlenc#aes256-cbc): layout is [iv:16][ciphertext:n] + const int ivSize = 16; + + if (encryptedAssertionBytes.Length < ivSize) + throw new CryptographicException( + $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a CBC IV."); + + byte[] iv = encryptedAssertionBytes[..ivSize]; + byte[] cipherText = encryptedAssertionBytes[ivSize..]; + + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Key = symmetricKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + using var decryptor = aes.CreateDecryptor(); + byte[] plainText = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); + return Encoding.UTF8.GetString(plainText); + } } } \ No newline at end of file diff --git a/Tools.SAMLResponseDecryptor/Tools.SAMLResponseDecryptor.csproj b/Tools.SAMLResponseDecryptor/Tools.SAMLResponseDecryptor.csproj index 1431afc831..59062db1e8 100644 --- a/Tools.SAMLResponseDecryptor/Tools.SAMLResponseDecryptor.csproj +++ b/Tools.SAMLResponseDecryptor/Tools.SAMLResponseDecryptor.csproj @@ -7,4 +7,7 @@ Tools.SAMLResponseDecryptor Tools.SAMLResponseDecryptor + + + From e176a8554b9d5bae1d84bc3ad2cb551bd9b19834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Hoffmann=20S=C3=B8rensen?= Date: Tue, 9 Jun 2026 20:14:55 +0200 Subject: [PATCH 2/2] Minor cleanup --- Tools.SAMLResponseDecryptor/SAMLDecryptor.cs | 142 +++++-------------- 1 file changed, 38 insertions(+), 104 deletions(-) diff --git a/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs b/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs index ab366100ab..af93055f70 100644 --- a/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs +++ b/Tools.SAMLResponseDecryptor/SAMLDecryptor.cs @@ -2,138 +2,72 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; -using Spectre.Console; public class SAMLDecryptor { public static byte[] DecryptSymmetricKey(string encryptedKeyBase64, string privateKeyPath, string certificatePassword) { - var keyStorageAttempts = new[] - { - (X509KeyStorageFlags.EphemeralKeySet, "EphemeralKeySet"), - (X509KeyStorageFlags.DefaultKeySet, "DefaultKeySet"), - (X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable, "MachineKeySet+Exportable"), - }; + // X509CertificateLoader is the non-obsolete replacement for the X509Certificate2 constructor (SYSLIB0057) + var certificate = X509CertificateLoader.LoadPkcs12FromFile( + privateKeyPath, + certificatePassword, + X509KeyStorageFlags.EphemeralKeySet); - var paddingModes = new[] - { - (RSAEncryptionPadding.OaepSHA1, "OAEP SHA-1"), - (RSAEncryptionPadding.OaepSHA256, "OAEP SHA-256"), - (RSAEncryptionPadding.Pkcs1, "PKCS#1 v1.5"), - }; + // GetRSAPrivateKey() returns null if the certificate has no RSA private key (CS8602) + var rsa = certificate.GetRSAPrivateKey() + ?? throw new CryptographicException("Certificate does not contain an RSA private key."); byte[] encryptedKeyBytes = Convert.FromBase64String(encryptedKeyBase64); - foreach (var (storageFlags, _) in keyStorageAttempts) + var paddingModes = new[] + { + (RSAEncryptionPadding.OaepSHA1, "OAEP SHA-1 (rsa-oaep-mgf1p + sha1)"), // matches EncryptionMethod rsa-oaep-mgf1p / DigestMethod sha1 + (RSAEncryptionPadding.OaepSHA256, "OAEP SHA-256"), + (RSAEncryptionPadding.Pkcs1, "PKCS#1 v1.5"), + }; + + foreach (var (padding, name) in paddingModes) { - X509Certificate2 certificate; try { - // X509CertificateLoader is the non-obsolete replacement for the X509Certificate2 constructor (SYSLIB0057) - certificate = X509CertificateLoader.LoadPkcs12FromFile(privateKeyPath, certificatePassword, storageFlags); + byte[] symmetricKey = rsa.Decrypt(encryptedKeyBytes, padding); + Console.WriteLine($"[OK] Decrypted with padding: {name}"); + return symmetricKey; } catch (CryptographicException) { - continue; - } - - var rsa = certificate.GetRSAPrivateKey(); - if (rsa is null) - continue; - - int rsaKeySizeBytes = rsa.KeySize / 8; - if (rsaKeySizeBytes != encryptedKeyBytes.Length) - AnsiConsole.MarkupLine( - $"[yellow]Warning:[/] RSA key is {rsaKeySizeBytes} bytes but encrypted key is {encryptedKeyBytes.Length} bytes — decryption may fail."); - - foreach (var (padding, paddingName) in paddingModes) - { - try - { - byte[] symmetricKey = rsa.Decrypt(encryptedKeyBytes, padding); - - var table = new Table().NoBorder().HideHeaders(); - table.AddColumn("k").AddColumn("v"); - table.AddRow("[grey]Subject[/]", Markup.Escape(certificate.Subject)); - table.AddRow("[grey]Serial[/]", certificate.SerialNumber); - table.AddRow("[grey]Key[/]", $"{rsa.KeySize} bits · {paddingName}"); - AnsiConsole.Write(table); - AnsiConsole.MarkupLine( - $"[green]Symmetric key decrypted[/] [grey]({symmetricKey.Length * 8}-bit)[/]"); - - return symmetricKey; - } - catch (CryptographicException) - { - // try next padding - } + Console.WriteLine($"[FAIL] Padding {name} did not work."); } } - throw new CryptographicException( - "Failed to decrypt the symmetric key with any combination of key storage flags and RSA padding mode."); + throw new CryptographicException("Failed to decrypt the symmetric key with any known RSA padding mode."); } - public static string DecryptAssertion(string encryptedAssertionBase64, byte[] symmetricKey, string? encryptionAlgorithm = null) + public static string DecryptAssertion(string encryptedAssertionBase64, byte[] symmetricKey) { byte[] encryptedAssertionBytes = Convert.FromBase64String(encryptedAssertionBase64); - const string gcmUri = "http://www.w3.org/2009/xmlenc11#aes256-gcm"; + // AES-256-CBC (xmlenc#aes256-cbc): layout is [iv:16][ciphertext:n] + const int ivSize = 16; // CBC IV: 128 bits - bool useGcm = string.Equals(encryptionAlgorithm, gcmUri, StringComparison.OrdinalIgnoreCase) - || string.IsNullOrEmpty(encryptionAlgorithm); // auto-detect: try GCM first + if (encryptedAssertionBytes.Length < ivSize) + throw new CryptographicException( + $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a CBC IV."); - if (useGcm) - { - // AES-256-GCM (xmlenc11#aes256-gcm): layout is [nonce:12][ciphertext:n][tag:16] - const int nonceSize = 12; - const int tagSize = 16; - - if (encryptedAssertionBytes.Length >= nonceSize + tagSize) - { - try - { - byte[] nonce = encryptedAssertionBytes[..nonceSize]; - byte[] cipherText = encryptedAssertionBytes[nonceSize..^tagSize]; - byte[] tag = encryptedAssertionBytes[^tagSize..]; - - using var aesGcm = new AesGcm(symmetricKey, tagSizeInBytes: tagSize); - byte[] plainText = new byte[cipherText.Length]; - aesGcm.Decrypt(nonce, cipherText, tag, plainText); - return Encoding.UTF8.GetString(plainText); - } - catch (CryptographicException) when (string.IsNullOrEmpty(encryptionAlgorithm)) - { - // GCM failed during auto-detection — fall through to CBC - } - } + byte[] iv = encryptedAssertionBytes[..ivSize]; + byte[] cipherText = encryptedAssertionBytes[ivSize..]; - // If algorithm was explicitly GCM but data is too short, give a clear error - if (!string.IsNullOrEmpty(encryptionAlgorithm)) - throw new CryptographicException( - $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a GCM nonce and tag."); - } - - { - // AES-256-CBC (xmlenc#aes256-cbc): layout is [iv:16][ciphertext:n] - const int ivSize = 16; + Console.WriteLine($"[DEBUG] CBC iv={ivSize}B, ciphertext={cipherText.Length}B"); - if (encryptedAssertionBytes.Length < ivSize) - throw new CryptographicException( - $"Assertion data too short ({encryptedAssertionBytes.Length} bytes) to contain a CBC IV."); + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Key = symmetricKey; + aes.IV = iv; + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; - byte[] iv = encryptedAssertionBytes[..ivSize]; - byte[] cipherText = encryptedAssertionBytes[ivSize..]; + using var decryptor = aes.CreateDecryptor(); + byte[] plainText = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); - using var aes = System.Security.Cryptography.Aes.Create(); - aes.Key = symmetricKey; - aes.IV = iv; - aes.Mode = CipherMode.CBC; - aes.Padding = PaddingMode.PKCS7; - - using var decryptor = aes.CreateDecryptor(); - byte[] plainText = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length); - return Encoding.UTF8.GetString(plainText); - } + return Encoding.UTF8.GetString(plainText); } } \ No newline at end of file