Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public byte[] EncryptBytes(byte[] input, EncryptionAlgorithm algorithm, Symmetri
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(options);
ValidateSymmetric(algorithm, options, options.IV);
ValidateSymmetric(algorithm, options, options.IV, isDecrypt: false);
return options.Key.UseKeyBytes(keyBytes =>
SymmetricInteropHelper.DispatchEncrypt(algorithm, options.Format, options.KdfIterations, keyBytes, options.IV, input, options.AesKeySize));
}
Expand All @@ -26,7 +26,7 @@ public byte[] DecryptBytes(byte[] input, EncryptionAlgorithm algorithm, Symmetri
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(options);
ValidateSymmetric(algorithm, options, iv: null);
ValidateSymmetric(algorithm, options, iv: null, isDecrypt: true);
return options.Key.UseKeyBytes(keyBytes =>
SymmetricInteropHelper.DispatchDecrypt(algorithm, options.Format, options.KdfIterations, keyBytes, input, options.AesKeySize));
}
Expand Down Expand Up @@ -305,12 +305,14 @@ public bool PgpVerifyPublicKey(PgpPublicKey key)
// enforced at compile time by the typed factory parameters on SymmetricEncryptOptions /
// SymmetricDecryptOptions; this still catches KDF-iteration-out-of-bounds, raw-key
// length mismatch, and (encrypt) IV-on-non-Raw-format. iv is null for decrypt.
private static void ValidateSymmetric(EncryptionAlgorithm algorithm, CryptoOptions options, byte[] iv)
// isDecrypt skips the encrypt-only positive-but-below-minimum KDF-iteration floor so a
// third-party blob produced with a low iteration count can still be decrypted (STUD-80534).
private static void ValidateSymmetric(EncryptionAlgorithm algorithm, CryptoOptions options, byte[] iv, bool isDecrypt)
{
CryptoKey key = options.Key ?? throw new ArgumentException("Options must carry a key (construct via a format factory).", nameof(options));
string ivSentinel = iv != null && iv.Length > 0 ? "set" : null;
int? rawKeyLength = key.IsRawKey ? key.KeyBytes.Length : (int?)null;
SymmetricInteropHelper.ValidateInteropSettings(algorithm, options.Format, key.BytesFormat, ivSentinel, options.KdfIterations, rawKeyLength);
SymmetricInteropHelper.ValidateInteropSettings(algorithm, options.Format, key.BytesFormat, ivSentinel, options.KdfIterations, rawKeyLength, isDecrypt);
}

private static string ComputeHashHex(KeyedHashAlgorithms algorithm, byte[] inputBytes, byte[] keyBytes)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,59 @@ public void OpenSslEnc_AesCbc_InternalToExternal(int iterations, AesKeySize aesK
Assert.Equal(Plaintext, Encoding.UTF8.GetString(decrypted));
}

// ────────────────────────────────────────────────────────────────────────
// STUD-80534 — the MinKdfIterations=1000 floor is an encrypt-only guard. A
// third-party blob produced with a low iteration count (e.g.
// `openssl enc -pbkdf2 -iter 100`) is mathematically decryptable, so decrypt
// must honour whatever the producer chose; encrypt must still refuse to emit
// weak ciphertext.
// ────────────────────────────────────────────────────────────────────────

[Fact]
public void OpenSslEnc_AesCbc_DecryptHonoursLowKdfIterations()
{
// Reproducer: an external producer used iterations=100 (below the 1000 floor). We can
// derive the matching key/IV, so the round-trip must succeed. Before the fix this threw
// ArgumentException (Validation_KdfIterations_BelowMinimum) before reaching the decrypt code.
const int lowIterations = 100;
byte[] plainBytes = Encoding.UTF8.GetBytes(Plaintext);
byte[] externalBlob = BclOpenSslEnc.EncryptAesCbc(plainBytes, Password, lowIterations, 32);

string decrypted = RunDecryptText(
EncryptionAlgorithm.AES, SymmetricWireFormat.OpenSslEnc, KeyBytesFormat.Encoded,
password: Password, input: Convert.ToBase64String(externalBlob),
inputEncoding: Encoding.UTF8, iterations: lowIterations, aesKeySize: AesKeySize.Aes256);

Assert.Equal(Plaintext, decrypted);
}

[Fact]
public void OpenSslEnc_AesCbc_EncryptStillRefusesLowKdfIterations()
{
// Encrypt-side floor pin: we must never produce ciphertext with a weak iteration count.
Assert.Throws<ArgumentException>(() => RunEncryptText(
EncryptionAlgorithm.AES, SymmetricWireFormat.OpenSslEnc, KeyBytesFormat.Encoded,
password: Password, inputEncoding: Encoding.UTF8, iterations: 100, aesKeySize: AesKeySize.Aes256));
}

[Fact]
public void OpenSslEnc_AesCbc_NegativeKdfIterations_RejectedOnBothDirections()
{
// Negative iterations have no legitimate use case and stay rejected regardless of direction.
// On decrypt the validator runs before the input blob is decoded, so the input is never read;
// and a junk-blob failure would surface as InvalidOperationException, not ArgumentException —
// asserting ArgumentException therefore pins the rejection to the iteration validator alone.
Assert.Throws<ArgumentException>(() => RunEncryptText(
EncryptionAlgorithm.AES, SymmetricWireFormat.OpenSslEnc, KeyBytesFormat.Encoded,
password: Password, inputEncoding: Encoding.UTF8, iterations: -1, aesKeySize: AesKeySize.Aes256));

byte[] validBlob = BclOpenSslEnc.EncryptAesCbc(Encoding.UTF8.GetBytes(Plaintext), Password, 600_000, 32);
Assert.Throws<ArgumentException>(() => RunDecryptText(
EncryptionAlgorithm.AES, SymmetricWireFormat.OpenSslEnc, KeyBytesFormat.Encoded,
password: Password, input: Convert.ToBase64String(validBlob),
inputEncoding: Encoding.UTF8, iterations: -1, aesKeySize: AesKeySize.Aes256));
}

// ────────────────────────────────────────────────────────────────────────
// STUD-80530 — plaintext encoding must be governed by PlaintextEncoding, NOT the
// key/password Encoding. These two tests would pass against a UiPath↔UiPath round-trip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,26 @@ public void Validate_KdfIterations_AtFloor(int iterations, bool shouldThrow)
else Should.NotThrow(act);
}

// STUD-80534: the positive-but-below-minimum floor is an encrypt-only guard. On decrypt
// (isDecrypt: true) a low iteration count chosen by a third-party producer must be honoured,
// so 999 no longer throws. Negative iterations have no legitimate use case and stay rejected
// in both directions; zero still means "use the format default" in both directions.
[Theory]
[InlineData(999, false)]
[InlineData(1_000, false)]
[InlineData(0, false)]
[InlineData(-1, true)]
[InlineData(int.MinValue, true)]
public void Validate_KdfIterations_AtFloor_DecryptSkipsPositiveFloor(int iterations, bool shouldThrow)
{
Action act = () => SymmetricInteropHelper.ValidateInteropSettings(
EncryptionAlgorithm.AES, SymmetricWireFormat.Owasp2026, KeyBytesFormat.Encoded,
ivString: null, kdfIterations: iterations, rawKeyLengthBytes: null, isDecrypt: true);

if (shouldThrow) Should.Throw<ArgumentException>(act);
else Should.NotThrow(act);
}

[Fact]
public void Validate_Raw_WithWrongKeyLength_ThrowsWithLegalSizesInMessage()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,8 @@ private byte[] ExecuteSymmetricDecrypt(CodeActivityContext context, byte[] encry
{
throw new InvalidOperationException(Resources.GenericCryptographicException, ex);
}
});
},
isDecrypt: true);
}

private void WriteDecryptedOutput(CodeActivityContext context, string outputFilePath, byte[] decrypted, (string, string, string) result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ private string ExecuteSymmetricDecrypt(CodeActivityContext context, string input
{
throw new InvalidOperationException(Resources.GenericCryptographicException, ex);
}
});
},
isDecrypt: true);

return plaintextEncoding.GetString(decrypted);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
KeyBytesFormat keyFormat,
string ivString,
int kdfIterations,
int? rawKeyLengthBytes)
int? rawKeyLengthBytes,
bool isDecrypt = false)
{
if (format == SymmetricWireFormat.Raw && keyFormat == KeyBytesFormat.Encoded)
throw new ArgumentException(Resources.Validation_RawKeyFormat_EncodedNotAllowed);
Expand All @@ -44,7 +45,10 @@
throw new ArgumentException(Resources.Validation_KdfIterations_NotForClassicOrRaw);
// Negative iterations were silently swallowed by the dispatch (treated as "use default") —
// only zero means "default". Anything below the minimum (including negatives) is invalid.
if (kdfIterations < 0 || (kdfIterations > 0 && kdfIterations < MinKdfIterations))
// The positive-but-below-minimum floor is an encrypt-only guard (never produce weak ciphertext);

Check warning on line 48 in Activities/Cryptography/UiPath.Cryptography/SymmetricInteropHelper.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=UiPath_Community.Activities&issues=AZ7hjIKwahmNBK0Sgc_c&open=AZ7hjIKwahmNBK0Sgc_c&pullRequest=577
// on decrypt we must honour whatever iteration count a third-party producer chose, so the floor
// is skipped when isDecrypt is true. Negative iterations stay rejected in both directions.
if (kdfIterations < 0 || (kdfIterations > 0 && kdfIterations < MinKdfIterations && !isDecrypt))
throw new ArgumentException(string.Format(Resources.Validation_KdfIterations_BelowMinimum, kdfIterations, MinKdfIterations));

if (format == SymmetricWireFormat.Raw && rawKeyLengthBytes.HasValue)
Expand Down Expand Up @@ -137,19 +141,20 @@
string ivString,
int kdfIterations,
bool needsIv,
Func<byte[], byte[], TOut> dispatch)
Func<byte[], byte[], TOut> dispatch,
bool isDecrypt = false)
Comment thread
alexandru-petre marked this conversation as resolved.
{
if (dispatch == null) throw new ArgumentNullException(nameof(dispatch));

ValidateInteropSettings(algorithm, format, keyFormat, ivString, kdfIterations, null);
ValidateInteropSettings(algorithm, format, keyFormat, ivString, kdfIterations, null, isDecrypt);

byte[] keyOrPasswordBytes = ParseKeyOrIv(keyString, keySecureString, keyFormat, keyEncoding);
byte[] ivBytes = needsIv
? ParseKeyOrIv(ivString, null, keyFormat, keyEncoding)
: null;

if (format == SymmetricWireFormat.Raw)
ValidateInteropSettings(algorithm, format, keyFormat, ivString, kdfIterations, keyOrPasswordBytes?.Length);
ValidateInteropSettings(algorithm, format, keyFormat, ivString, kdfIterations, keyOrPasswordBytes?.Length, isDecrypt);

try
{
Expand Down