diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java index 92576f65a..4c7f94f24 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java @@ -21,17 +21,7 @@ import com.ibm.engine.language.csharp.tree.CSharpTree; import com.ibm.engine.rule.IDetectionRule; -import com.ibm.plugin.rules.detection.dotnet.DotNetAES; -import com.ibm.plugin.rules.detection.dotnet.DotNetDES; -import com.ibm.plugin.rules.detection.dotnet.DotNetDSA; -import com.ibm.plugin.rules.detection.dotnet.DotNetECDiffieHellman; -import com.ibm.plugin.rules.detection.dotnet.DotNetECDsa; -import com.ibm.plugin.rules.detection.dotnet.DotNetHMAC; -import com.ibm.plugin.rules.detection.dotnet.DotNetRC2; -import com.ibm.plugin.rules.detection.dotnet.DotNetRSA; -import com.ibm.plugin.rules.detection.dotnet.DotNetRfc2898DeriveBytes; -import com.ibm.plugin.rules.detection.dotnet.DotNetSHA; -import com.ibm.plugin.rules.detection.dotnet.DotNetTripleDES; +import com.ibm.plugin.rules.detection.dotnet.*; import java.util.List; import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -47,6 +37,7 @@ private CSharpDetectionRules() { public static List> rules() { return Stream.of( DotNetAES.rules().stream(), + DotNetChaCha20Poly1305.rules().stream(), DotNetDES.rules().stream(), DotNetTripleDES.rules().stream(), DotNetRC2.rules().stream(), diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305.java new file mode 100644 index 000000000..c33507613 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305.java @@ -0,0 +1,79 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import javax.annotation.Nonnull; + +public final class DotNetChaCha20Poly1305 { + private DotNetChaCha20Poly1305() { + // nothing + } + + // chaCha20Poly1305.Encrypt(nonce, plaintext, ciphertext, tag [, associatedData]) + private static final IDetectionRule CHACHA20POLY1305_ENCRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Encrypt") + .shouldBeDetectedAs(new ValueActionFactory<>("ENCRYPT")) + .withAnyParameters() // Byte[] or ReadOnlySpan overloads + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // chaCha20Poly1305.Decrypt(nonce, ciphertext, tag, plaintext [, associatedData]) + private static final IDetectionRule CHACHA20POLY1305_DECRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Decrypt") + .shouldBeDetectedAs(new ValueActionFactory<>("DECRYPT")) + .withAnyParameters() // Byte[] or ReadOnlySpan overloads + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> CHACHA20POLY1305_OP_RULES = + List.of(CHACHA20POLY1305_ENCRYPT_OP, CHACHA20POLY1305_DECRYPT_OP); + + // new ChaCha20Poly1305(key) + private static final IDetectionRule CHACHA20POLY1305 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ChaCha20Poly1305") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("CHACHA20POLY1305")) + .withAnyParameters() // Byte[] or ReadOnlySpan, 1 parameter + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(CHACHA20POLY1305_OP_RULES); + + @Nonnull + public static List> rules() { + return List.of(CHACHA20POLY1305); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java index 4fa746e89..c316f5c34 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java @@ -34,11 +34,9 @@ import com.ibm.mapper.mapper.jca.JcaPaddingMapper; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.KeyLength; -import com.ibm.mapper.model.algorithms.AES; -import com.ibm.mapper.model.algorithms.DES; -import com.ibm.mapper.model.algorithms.DESede; -import com.ibm.mapper.model.algorithms.RC2; -import com.ibm.mapper.model.algorithms.RSA; +import com.ibm.mapper.model.algorithms.*; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; import com.ibm.mapper.utils.DetectionLocation; import java.util.Optional; import javax.annotation.Nonnull; @@ -63,11 +61,15 @@ public final class CSharpCipherContextTranslator Optional result = switch (valueStr) { case "AES" -> Optional.of(new AES(detectionLocation)); + case "CHACHA20POLY1305" -> + Optional.of(new ChaCha20Poly1305(detectionLocation)); case "DES" -> Optional.of(new DES(detectionLocation)); case "3DES", "DESEDE", "TRIPLEDES" -> Optional.of(new DESede(detectionLocation)); case "RSA" -> Optional.of(new RSA(detectionLocation)); case "RC2" -> Optional.of(new RC2(detectionLocation)); + case "ENCRYPT" -> Optional.of(new Encrypt(detectionLocation)); + case "DECRYPT" -> Optional.of(new Decrypt(detectionLocation)); default -> Optional.empty(); }; if (result.isPresent()) { diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetChaCha20Poly1305TestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetChaCha20Poly1305TestFile.cs new file mode 100644 index 000000000..25069d6ec --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetChaCha20Poly1305TestFile.cs @@ -0,0 +1,158 @@ +/* + * Test file for System.Security.Cryptography.ChaCha20Poly1305 detection rules. + * + * Covers the full API surface of the ChaCha20Poly1305 class: + * - constructor overloads (byte[] key, ReadOnlySpan key) + * - Encrypt overloads (byte[] and ReadOnlySpan), with and without associatedData + * - Decrypt overloads (byte[] and ReadOnlySpan), with and without associatedData + * + * Architecture note: Encrypt/Decrypt are depending rules attached to the constructor + * (see DotNetChaCha20Poly1305.java), so each method below that constructs-then-uses + * the object produces a single finding with nested Encrypt/Decrypt children, not separate + * disconnected findings. + */ + +using System.Security.Cryptography; + +public class DotNetChaCha20Poly1305ComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Constructor overloads + // ------------------------------------------------------------------------- + + public void TestConstructByteArrayKey() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + } + + public void TestConstructSpanKey() + { + var key = new ReadOnlySpan[32]; + var chaCha = new ChaCha20Poly1305(key); + } + + // ------------------------------------------------------------------------- + // Section 2: Encrypt — byte[] overload + // ------------------------------------------------------------------------- + + public void TestEncryptByteArrayWithAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new byte[12]; + var plainText = new byte[32]; + var cipherText = new byte[32]; + var tag = new byte[16]; + var associatedData = new byte[32]; + + chaCha.Encrypt(nonce, plainText, cipherText, tag, associatedData); + } + + public void TestEncryptByteArrayNoAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new byte[12]; + var plainText = new byte[32]; + var cipherText = new byte[32]; + var tag = new byte[16]; + + chaCha.Encrypt(nonce, plainText, cipherText, tag); + } + + // ------------------------------------------------------------------------- + // Section 3: Encrypt — ReadOnlySpan overload + // ------------------------------------------------------------------------- + + public void TestEncryptSpanWithAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new ReadOnlySpan[12]; + var plainText = new ReadOnlySpan[32]; + var cipherText = new ReadOnlySpan[32]; + var tag = new ReadOnlySpan[16]; + var associatedData = new ReadOnlySpan[32]; + + chaCha.Encrypt(nonce, plainText, cipherText, tag, associatedData); + } + + public void TestEncryptSpanNoAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new ReadOnlySpan[12]; + var plainText = new ReadOnlySpan[32]; + var cipherText = new ReadOnlySpan[32]; + var tag = new ReadOnlySpan[16]; + + chaCha.Encrypt(nonce, plainText, cipherText, tag); + } + + // ------------------------------------------------------------------------- + // Section 4: Decrypt — byte[] overload + // ------------------------------------------------------------------------- + + public void TestDecryptByteArrayWithAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new byte[12]; + var cipherText = new byte[32]; + var tag = new byte[16]; + var plainText = new byte[32]; + var associatedData = new byte[32]; + + chaCha.Decrypt(nonce, cipherText, tag, plainText, associatedData); + } + + public void TestDecryptByteArrayNoAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new byte[12]; + var cipherText = new byte[32]; + var tag = new byte[16]; + var plainText = new byte[32]; + + chaCha.Decrypt(nonce, cipherText, tag, plainText); + } + + // ------------------------------------------------------------------------- + // Section 5: Decrypt — ReadOnlySpan overload + // ------------------------------------------------------------------------- + + public void TestDecryptSpanWithAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new ReadOnlySpan[12]; + var cipherText = new ReadOnlySpan[32]; + var tag = new ReadOnlySpan[16]; + var plainText = new ReadOnlySpan[32]; + var associatedData = new ReadOnlySpan[32]; + + chaCha.Decrypt(nonce, cipherText, tag, plainText, associatedData); + } + + public void TestDecryptSpanNoAad() + { + var key = new byte[32]; + var chaCha = new ChaCha20Poly1305(key); + + var nonce = new ReadOnlySpan[12]; + var cipherText = new ReadOnlySpan[32]; + var tag = new ReadOnlySpan[16]; + var plainText = new ReadOnlySpan[32]; + + chaCha.Decrypt(nonce, cipherText, tag, plainText); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305Test.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305Test.java new file mode 100644 index 000000000..ded891e24 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetChaCha20Poly1305Test.java @@ -0,0 +1,175 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2026 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.AuthenticatedEncryption; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Test for all ChaCha20Poly1305-related detection rules + * (DotNetChaCha20Poly1305.java). + * + *

Covers the full API surface of {@code System.Security.Cryptography.ChaCha20Poly1305}: + * + *

    + *
  • constructor overloads (byte[] key, ReadOnlySpan<byte> key) + *
  • Encrypt overloads (byte[] and ReadOnlySpan<byte>), with and without associatedData + *
  • Decrypt overloads (byte[] and ReadOnlySpan<byte>), with and without associatedData + *
+ * + *

Finding mapping (one finding per test method in + * DotNetChaCha20Poly1305TestFile.cs): + * + *

+ * Section 1 – constructor overloads (findings 0–1):
+ *   0  TestConstructByteArrayKey             → ChaCha20-Poly1305
+ *   1  TestConstructSpanKey                  → ChaCha20-Poly1305
+ *
+ * Section 2 – Encrypt, byte[] overload (findings 2–3):
+ *   2  TestEncryptByteArrayWithAad           → ChaCha20-Poly1305 + Encrypt
+ *   3  TestEncryptByteArrayNoAad             → ChaCha20-Poly1305 + Encrypt
+ *
+ * Section 3 – Encrypt, ReadOnlySpan<byte> overload (findings 4–5):
+ *   4  TestEncryptSpanWithAad                → ChaCha20-Poly1305 + Encrypt
+ *   5  TestEncryptSpanNoAad                  → ChaCha20-Poly1305 + Encrypt
+ *
+ * Section 4 – Decrypt, byte[] overload (findings 6–7):
+ *   6  TestDecryptByteArrayWithAad           → ChaCha20-Poly1305 + Decrypt
+ *   7  TestDecryptByteArrayNoAad             → ChaCha20-Poly1305 + Decrypt
+ *
+ * Section 5 – Decrypt, ReadOnlySpan<byte> overload (findings 8–9):
+ *   8  TestDecryptSpanWithAad                → ChaCha20-Poly1305 + Decrypt
+ *   9  TestDecryptSpanNoAad                  → ChaCha20-Poly1305 + Decrypt
+ *
+ * 
+ */ +class DotNetChaCha20Poly1305Test extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify( + "rules/detection/dotnet/DotNetChaCha20Poly1305TestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be CHACHA20POLY1305 + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("CHACHA20POLY1305"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(AuthenticatedEncryption.class); + assertThat(node.asString()).isEqualTo("ChaCha20-Poly1305"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: constructor overloads — no Encrypt/Decrypt children + // ----------------------------------------------------------------- + case 0, 1 -> { + assertThat(node.getChildren().get(Encrypt.class)).isNull(); + assertThat(node.getChildren().get(Decrypt.class)).isNull(); + } + + // ----------------------------------------------------------------- + // Section 2: Encrypt — byte[] overload + // ----------------------------------------------------------------- + case 2, 3 -> assertEncryptFindings(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 3: Encrypt — ReadOnlySpan overload + // ----------------------------------------------------------------- + case 4, 5 -> assertEncryptFindings(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 4: Decrypt — byte[] overload + // ----------------------------------------------------------------- + case 6, 7 -> assertDecryptFindings(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 5: Decrypt — ReadOnlySpan overload + // ----------------------------------------------------------------- + case 8, 9 -> assertDecryptFindings(detectionStore, node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertEncryptFindings( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore encryptStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + assertThat(node.getChildren().get(Decrypt.class)).isNull(); + } + + private void assertDecryptFindings( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore decryptStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(node.getChildren().get(Decrypt.class)).isNotNull(); + assertThat(node.getChildren().get(Encrypt.class)).isNull(); + } +}