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
13 changes: 9 additions & 4 deletions lib/src/impl_ffi/impl_ffi.utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ void _checkOp(bool condition, {String? message, String? fallback}) {
message ??= err ?? fallback ?? 'unknown error';
throw operationError(message);
}
ssl.ERR_clear_error();
}

/// Throw [OperationError] if [retval] is not `1`.
Expand All @@ -113,6 +114,7 @@ void _checkData(bool condition, {String? message, String? fallback}) {
message ??= err ?? fallback ?? 'unknown error';
throw FormatException(message);
}
ssl.ERR_clear_error();
}

/// Throw [FormatException] if [retval] is `1`.
Expand Down Expand Up @@ -270,6 +272,7 @@ class _Scope implements Allocator {
return await fn(scope);
} finally {
scope._release();
ssl.ERR_clear_error();
}
}

Expand All @@ -281,6 +284,7 @@ class _Scope implements Allocator {
yield* fn(scope);
} finally {
scope._release();
ssl.ERR_clear_error();
}
}

Expand All @@ -294,6 +298,7 @@ class _Scope implements Allocator {
return fn(scope);
} finally {
scope._release();
ssl.ERR_clear_error();
}
}
}
Expand Down Expand Up @@ -428,10 +433,10 @@ Future<bool> _verifyStream(
signature.length,
);
if (result != 1) {
// TODO: We should always clear errors, when returning from any
// function that uses BoringSSL.
// Note: In this case we could probably assert that error is just
// signature related.
// Always clear errors, so we don't leak anything from the error queue.
ssl.ERR_clear_error();
} else {
// Also clear errors on success, just in case.
ssl.ERR_clear_error();
}
return result == 1;
Expand Down
85 changes: 85 additions & 0 deletions test/thread_local_error_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
@TestOn('vm')
library thread_local_error_test;

import 'dart:typed_data';

import 'package:test/test.dart';
import 'package:webcrypto/webcrypto.dart';

// This relies on internal implementation details to check for error leaks.
// If implementation changes significantly, this test might need updates.
import 'package:webcrypto/src/boringssl/lookup/lookup.dart'; // Access internal ssl

void main() {
test('BoringSSL error stack is empty after operations', () async {
// Helper to check error stack
void checkErrorStack() {
// We peep at the error stack to see if anything was left behind.
// Operations must clean up after themselves.
final err = ssl.ERR_peek_error();
if (err != 0) {
// Just failing with the error code is sufficient to signal a leak.
fail('BoringSSL error stack not empty. Error code: $err');
}
}

// Initial check to ensure clean slate
checkErrorStack();

// 1. Digest (SHA-256)
await Hash.sha256.digestBytes(Uint8List(10));
checkErrorStack();

// 2. HMAC Generation & Sign & Verify
final hmacKey = await HmacSecretKey.generateKey(Hash.sha256);
checkErrorStack();
final signature = await hmacKey.signBytes(Uint8List(10));
checkErrorStack();
final isValid = await hmacKey.verifyBytes(signature, Uint8List(10));
expect(isValid, isTrue);
checkErrorStack();

// 3. HMAC Verify Failure
// Flip a bit in signature to cause verification failure
final invalidSig = Uint8List.fromList(signature);
if (invalidSig.isNotEmpty) {
invalidSig[0] ^= 0xff;
}
final isInvalid = await hmacKey.verifyBytes(invalidSig, Uint8List(10));
expect(isInvalid, isFalse);
checkErrorStack();

// 4. AES-GCM
final aesKey = await AesGcmSecretKey.generateKey(256);
checkErrorStack();
final iv = Uint8List(12);
final encrypted = await aesKey.encryptBytes(Uint8List(10), iv);
checkErrorStack();
await aesKey.decryptBytes(encrypted, iv);
checkErrorStack();

// 5. ECDSA
final ecKey = await EcdsaPrivateKey.generateKey(EllipticCurve.p256);
checkErrorStack();
final ecSig = await ecKey.privateKey.signBytes(Uint8List(10), Hash.sha256);
checkErrorStack();
final ecValid = await ecKey.publicKey.verifyBytes(ecSig, Uint8List(10), Hash.sha256);
expect(ecValid, isTrue);
checkErrorStack();

// 6. Randomness
final randomBytes = Uint8List(32);
fillRandomBytes(randomBytes);
checkErrorStack();

// 7. Expected Failure (Import invalid JWK)
try {
// Missing 'k' property or invalid format
await AesGcmSecretKey.importJsonWebKey({'kty': 'oct', 'alg': 'A256GCM'});
fail('Should have thrown ArgumentError or FormatException');
} catch (_) {
// Expected exception
checkErrorStack();
}
});
}