Skip to content

[cryptor] support AES and SM4 encryption strategies - #6531

Open
im47cn wants to merge 3 commits into
apache:masterfrom
im47cn:feature/cryptor-aes-sm4-support
Open

[cryptor] support AES and SM4 encryption strategies#6531
im47cn wants to merge 3 commits into
apache:masterfrom
im47cn:feature/cryptor-aes-sm4-support

Conversation

@im47cn

@im47cn im47cn commented Aug 1, 2026

Copy link
Copy Markdown

Motivation

The cryptor plugin ships only RSA out of the box. Users needing symmetric ciphers (AES, SM4) have no built-in strategy today; SM4 is also required for Chinese national-standard (GM) compliance.

Modifications

  • New AesStrategy and Sm4Strategy (@Join) implementing the CryptorStrategy SPI, registered as aes and sm4.
  • Shared AbstractCbcCryptorStrategy encapsulates CBC wiring: key parsing, SecretKeySpec/IvParameterSpec init and BouncyCastle provider registration; subclasses only declare transformation + algorithm.
  • Key convention base64(secret):base64(iv), fixed AES|SM4/CBC/PKCS7Padding.
  • Added bcprov-jdk18on to the cryptor module.

No breaking changes: the CryptorStrategy interface, CryptorRuleHandler, admin and existing RSA rules are untouched.

Rule config example

strategyName=sm4, key=<base64-secret>:<base64-iv>

Tests

  • Parameterized round-trip (ASCII/CJK/JSON) and invalid-key-format cases for both strategies.
  • New strategy classes at 100% instruction coverage (Jacoco); existing RSA + plugin tests still pass (33 total).

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed #6531. No blockers. Two should-fix items and a few nits.

Should fix

  1. Static IV in CBC (security). AbstractCbcCryptorStrategy reuses the configured IV for every call (buildCipher, lines 62-71), and the documented key format is base64(secret):base64(iv) — i.e. one fixed (key, IV) pair per rule. Reusing a (key, IV) in CBC leaks first-block plaintext equality (identical JSON prefixes → identical first ciphertext blocks across requests) and opens chosen-plaintext paths. I know this matches the existing shenyu-common/AesUtils posture, but for a security plugin it's worth not silently propagating. At minimum, add a Javadoc warning that the IV must be unique per deployment/rule; ideally add a random-IV-per-message variant that prepends the IV to the ciphertext.

  2. No SPI-wiring regression test. Both new tests instantiate the strategy directly (new AesStrategy() / new Sm4Strategy()), so the META-INF SPI registration that the plugin actually depends on (CryptorStrategyFactory.newInstance("aes")ExtensionLoader.getJoin) is never exercised. If the aes=/sm4= line or the @Join annotation is dropped, these tests still pass and the plugin fails at runtime (silently — the factory catches and returns null). Please add a test that loads via CryptorStrategyFactory.newInstance("aes")/"sm4" and round-trips.

Nits

  1. Key-format divergence from AesUtils (raw UTF-8 string key/iv vs base64(secret):base64(iv)) is unmentioned and will confuse operators using the same secret across shenyu.aes.secret.* and the cryptor aes strategy. A Javadoc cross-reference would help.

  2. Negative tests cover separator/format errors only — no non-base64 content or wrong byte-length (15-byte AES secret, 17-byte SM4 key) cases.

  3. AesStrategyTest/Sm4StrategyTest vs the existing RSAStrategyTest naming — trivial style divergence.

The crypto wiring itself (explicit BC provider via Cipher.getInstance(transformation, "BC"), PKCS7Padding for AES/SM4, per-call Cipher, standard Base64 output decoded by the factory's MIME decoder) is correct, and the @Join/SPI file additions are right. The static-block provider registration with a getProvider null-guard is actually cleaner than AesUtils's unconditional addProvider per call.

private Cipher buildCipher(final int mode, final String secretBase64, final String ivBase64) throws Exception {
byte[] secret = Base64.getMimeDecoder().decode(secretBase64);
byte[] iv = Base64.getMimeDecoder().decode(ivBase64);
Cipher cipher = Cipher.getInstance(getTransformation(), BouncyCastleProvider.PROVIDER_NAME);
im47cn added a commit to im47cn/shenyu that referenced this pull request Aug 4, 2026
…ative test cases

Should-fix apache#1: Add Javadoc security warning on IV reuse in CBC mode,
including cross-reference to AesUtils key format divergence (nit apache#3).

Should-fix apache#2: Add CryptorStrategyFactorySpiTest that loads strategies
via CryptorStrategyFactory.newInstance() (exercising META-INF SPI +
ExtensionLoader.getJoin), not just direct instantiation.

Nit apache#4: Add negative tests for wrong byte-length keys (15-byte AES,
18-byte SM4) and non-base64 content.

apache#6531
@im47cn

im47cn commented Aug 4, 2026

Copy link
Copy Markdown
Author

Thanks @Aias00 for the thorough review. All items addressed in the latest push:

Should-fix #1 (IV reuse Javadoc): Added security warning to AbstractCbcCryptorStrategy class-level Javadoc documenting that the IV is fixed per rule, the CBC reuse risk, and operator guidance to regenerate IVs per deployment/rule.

Should-fix #2 (SPI-wiring regression test): Added CryptorStrategyFactorySpiTest that loads strategies via CryptorStrategyFactory.newInstance("aes") / newInstance("sm4") — exercising the real ExtensionLoader.getJoin + META-INF SPI path, not direct instantiation. If the SPI file or @Join annotation is dropped, these tests fail.

Nit #3 (key format divergence): Added cross-reference in the same Javadoc pointing to AesUtils and warning against interchanging secrets.

Nit #4 (negative test coverage): Added 3 new negative tests: AES 15-byte key (wrong length), SM4 18-byte key (wrong length), and non-base64 key content.

@Aias00

Aias00 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Strong test coverage (parameterized round-trip incl. CJK/JSON, wrong key-length, non-base64, SPI loadability). The SPI registration and the BouncyCastle provider guard are correct. One security design issue I think should be addressed before merge:

CBC with a fixed IV — IV reused across all messages. AbstractCbcCryptorStrategy.buildCipher parses the IV from the rule's key string (base64(secret):base64(iv)) and uses the same (key, IV) pair for every message encrypted under that rule. A gateway encrypts many requests/responses under one configured rule, so the same IV is reused per-message. CBC IV reuse leaks first-block plaintext equality and opens chosen-plaintext attack paths (CWE-329/CWE-1204) — and gateway request bodies are often partially attacker-controlled, which makes the CPA path reachable. The Javadoc acknowledges this ("Security note on IV reuse") and mentions a random-IV-per-message variant as a future enhancement, but I think it should be the default design rather than a TODO. Suggested fix: generate a fresh IV via SecureRandom per encrypt, prepend it to the ciphertext (base64(iv || ciphertext)), and parse it on decrypt — or switch to AES/GCM/NoPadding, which also gives authenticated encryption and resolves the malleability concern below.

CBC without authentication (malleable). AES/CBC/PKCS7Padding with no MAC means an attacker who can modify ciphertext in transit can flip bits to affect the decrypted plaintext (CBC bit-flipping). AES-GCM (above) would solve this; alternatively CBC+HMAC. The existing RSA strategy is also encryption-only, so this isn't a regression, but for new strategies it's worth flagging.

Minor: key material (base64(secret):base64(iv)) lives in the rule config and flows through admin/data-sync — consistent with RSA, so not a regression, but there's no KMS/vault integration for the new symmetric secrets. Also a small nit: encrypt uses Base64.getEncoder() (standard) while key/ciphertext parsing uses getMimeDecoder() (lenient, ignores line separators) — getDecoder() would be stricter and symmetric.

im47cn added 3 commits August 6, 2026 07:28
The cryptor plugin shipped only RSA out of the box. Add AesStrategy and
Sm4Strategy so the CryptorStrategy SPI covers symmetric ciphers too.

A shared AbstractCbcCryptorStrategy encapsulates the CBC wiring -- key
parsing, SecretKeySpec/IvParameterSpec init and BouncyCastle provider
registration -- while subclasses merely declare the transformation and
algorithm name.

Key convention: base64(secret):base64(iv), with a fixed
AES|SM4/CBC/PKCS7Padding transformation. The CryptorStrategy interface,
CryptorRuleHandler and admin stay untouched, so RSA and existing rules
keep working.

Tests: parameterized round-trip (ASCII/CJK/JSON payloads) plus invalid
key-format cases. New strategy classes at 100% instruction coverage.
…ative test cases

Should-fix apache#1: Add Javadoc security warning on IV reuse in CBC mode,
including cross-reference to AesUtils key format divergence (nit apache#3).

Should-fix apache#2: Add CryptorStrategyFactorySpiTest that loads strategies
via CryptorStrategyFactory.newInstance() (exercising META-INF SPI +
ExtensionLoader.getJoin), not just direct instantiation.

Nit apache#4: Add negative tests for wrong byte-length keys (15-byte AES,
18-byte SM4) and non-base64 content.

apache#6531
Replace CBC with a fixed IV by authenticated encryption throughout the
cryptor plugin to close the IV-reuse (CWE-329) and malleability
(CWE-1204) issues raised in review.

AES/SM4:
- Switch AES/CBC/PKCS7Padding and SM4/CBC/PKCS7Padding to GCM/NoPadding
  with a fresh 96-bit SecureRandom nonce per message and a 128-bit tag,
  emitted as base64(nonce || ciphertext || tag). Key format simplified
  to base64(secret) — GCM must never reuse a fixed IV, so the configured
  IV is removed and the legacy base64(secret):base64(iv) form is rejected.

RSA:
- Default 'rsa' strategy upgraded from PKCS#1 v1.5 to
  RSA/ECB/OAEPWithSHA-256AndMGF1Padding, with an explicit OAEPParameterSpec
  (MGF1 SHA-256) so the transformation is identical across JDKs/providers.
- New 'rsa-pkcs1' strategy keeps PKCS#1 v1.5 for legacy/external peers that
  cannot speak OAEP. Shared logic factored into AbstractRsaStrategy.

Misc:
- Unify Base64 encoder/decoder; decrypt now decodes UTF-8 explicitly.
- RSA test fixtures moved from 512-bit to 2048-bit (OAEP/SHA-256 cannot
  encrypt any plaintext under a 512-bit key).
- Refactor CryptorRequestPluginTest to shared key constants (DRY).

https://github.com/apache/shenyu/pr/6531
@im47cn
im47cn force-pushed the feature/cryptor-aes-sm4-support branch from a2da12d to 88b310d Compare August 6, 2026 07:28
@im47cn

im47cn commented Aug 6, 2026

Copy link
Copy Markdown
Author

Thanks @Aias00 — your points on IV reuse and missing authentication were spot on. Rather than documenting the risk, this push closes it. All three items addressed:

1. CBC IV reuse (CWE-329) → resolved by switching to GCM.
AES and SM4 now use AES/GCM/NoPadding / SM4/GCM/NoPadding with a fresh 96-bit SecureRandom nonce per message and a 128-bit tag. Output is base64(nonce ‖ ciphertext ‖ tag), so the nonce is unique per message — no fixed IV to reuse. The base64(secret):base64(iv) key form is gone (GCM must never reuse a fixed IV); the key is now just base64(secret), and the legacy secret:iv form is explicitly rejected with a clear error.

2. CBC malleability (CWE-1204) → resolved by GCM's authentication tag.
Any in-transit modification now fails decryption (AEADBadTagException) instead of silently producing corrupted plaintext. New tests assert both properties: shouldEmitDifferentCiphertextForRepeatedEncrypts (nonce freshness) and shouldFailAuthenticationWhenCiphertextIsTampered (integrity).

3. Base64 encoder/decoder asymmetry → fixed. Unified to getEncoder()/getDecoder() throughout; decrypt now decodes UTF-8 explicitly.

RSA — PKCS#1 v1.5 → OAEP, with a PKCS#1 fallback for your non-regression concern.
To address the padding-oracle weakness without breaking external peers that only speak PKCS#1 v1.5:

  • The default rsa strategy is now RSA/ECB/OAEPWithSHA-256AndMGF1Padding, with an explicit OAEPParameterSpec (MGF1 also SHA-256) so the transformation is identical across JDKs/providers.
  • A new rsa-pkcs1 strategy keeps PKCS#1 v1.5 — any rule interoperating with a legacy/external PKCS#1 system can opt in by strategy name, with no code change and no surprise breakage.

Shared RSA logic is factored into AbstractRsaStrategy; AbstractCbcCryptorStrategy is removed.

Note: RSA test fixtures moved from 512-bit to 2048-bit — OAEP/SHA-256 physically cannot encrypt any plaintext under a 512-bit key (keyBytes − 2·32 − 2 < 0), so the old fixture is no longer usable. AES/SM4 round-trip tests cover CJK + JSON payloads.

The full cryptor module suite (40 tests) is green locally (main project install + integrated-test test-compile both pass), including the SPI-wiring path you flagged earlier. Would appreciate another look when you have time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants