feat(swift-sdk): obfuscate runtime mnemonic bytes#3545
feat(swift-sdk): obfuscate runtime mnemonic bytes#3545QuantumExplorer merged 1 commit intov3.1-devfrom
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThe changes refactor mnemonic handling from String-based to UTF-8 byte-based APIs across wallet storage, signing, and derivation layers. New methods in WalletStorage enable efficient keychain presence checks and raw byte retrieval, while MnemonicResolverAndPersister introduces XOR-masking and memory scrubbing to prevent plaintext mnemonics from persisting in memory. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Review GateCommit:
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift (1)
755-774:⚠️ Potential issue | 🟠 Major
mnemonicUTF8Bytesandseedlinger in unscrubbable SwiftDataalong this path.The PR’s stated goal is to keep plaintext mnemonic bytes off the Swift heap (the new
MaskedMnemonicUTF8and the resolver-callback flow inKeychainSigner.signPlatformAddressOnDemandenforce that for the signing path). On this identity-key derivation path, however, the mnemonic UTF-8 bytes (Line 761) and the derived 64-byte BIP39seed(Line 770) both sit in plainDatafor the rest of the function with no scrub on exit, while the 32-byteprivateKeyis zeroed (Lines 810/837). Either:
- Mirror the resolver-based shape used by
dash_sdk_sign_with_mnemonic_resolver_and_pathand route this derivation through a single Rust entry point that takes the existingMnemonicResolver+ path + (out) keychain identifier, so the seed/mnemonic never materialize in a Swift heap object on this path; or- At minimum, copy
mnemonicUTF8Bytesandseedinto[UInt8]buffers andmemset_s-scrub them in adefer(same pattern asprivateKey), so this path matches the rest of the PR's defense-in-depth.The first option is the better fit with the swift-sdk guideline that forbids fetching the mnemonic from Keychain and round-tripping it across FFI for an operation Rust can complete end-to-end. As per coding guidelines: "Do not fetch the mnemonic from Keychain, hand it back to Rust, wait for derived bytes, and write those to Keychain—orchestrate the entire pipeline as a single FFI entry point in Rust instead."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift` around lines 755 - 774, deriveAndStoreIdentityKey currently fetches mnemonicUTF8Bytes via WalletStorage().retrieveMnemonicUTF8Bytes and calls Mnemonic.toSeed which leaves both mnemonicUTF8Bytes and seed as plain Swift Data on the heap; change this to avoid materializing plaintext: either (preferred) route the whole derivation into a single Rust FFI entry (e.g., implement a Rust function analogous to dash_sdk_sign_with_mnemonic_resolver_and_path that accepts a MnemonicResolver + derivation path + out keychain id and performs mnemonic→seed→identity-key derivation and storage end-to-end, then call that from deriveAndStoreIdentityKey instead of retrieving mnemonic bytes in Swift), or (minimal) immediately copy mnemonicUTF8Bytes and seed into [UInt8] buffers and add a defer {} that calls memset_s to scrub both buffers before returning (mirroring the existing privateKey scrub), and stop keeping the original Data around; reference WalletStorage.retrieveMnemonicUTF8Bytes, Mnemonic.toSeed, deriveAndStoreIdentityKey, MaskedMnemonicUTF8, KeychainSigner.signPlatformAddressOnDemand and MnemonicResolver when implementing the change.packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift (1)
145-194:⚠️ Potential issue | 🟡 MinorMasking is undermined by the un-scrubbed
mnemonicUTF8BytesDataheld alongside it.
mnemonicUTF8Bytes(Line 147) is aDatawhose backing storage is not zeroable, and it stays live for the full duration of thisresolvecall — i.e., for the entire window during whichMaskedMnemonicUTF8is also alive. Since both copies coexist in memory until function return, the XOR-masked copy provides no defensive benefit on this path: anything that can sweep the heap duringresolvecan read the plaintext directly from theDatabacking.If the goal is genuinely to keep plaintext off the heap in this resolver, consider having
MaskedMnemonicUTF8.initconsume aninout [UInt8](caller-owned, scrub-on-defer) and copying/scrubbing the bytes out of the keychainDatainto that[UInt8]buffer immediately at line 147, e.g.:♻️ Sketch: scrub the keychain `Data` at the boundary
- let mnemonicUTF8Bytes: Data + var mnemonicBytes: [UInt8] do { - mnemonicUTF8Bytes = try storage.retrieveMnemonicUTF8Bytes(for: walletId) + let raw = try storage.retrieveMnemonicUTF8Bytes(for: walletId) + mnemonicBytes = [UInt8](raw) + // `raw` (Data) is dropped here; only `mnemonicBytes` remains. } catch WalletStorageError.mnemonicNotFound { ... } ... + defer { scrubBytes(&mnemonicBytes) } let maskedMnemonic: MaskedMnemonicUTF8 do { - maskedMnemonic = try MaskedMnemonicUTF8(plaintextUTF8Bytes: mnemonicUTF8Bytes) + maskedMnemonic = try MaskedMnemonicUTF8(plaintextBytes: &mnemonicBytes) } catch { return .other }(Note that even with this, the
Datareturned fromSecItemCopyMatchingis still copied internally by Foundation; reducing the surface to one[UInt8]buffer that you can reliablymemset_sis the best Swift can offer here.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift` around lines 145 - 194, The stored plaintext Data (mnemonicUTF8Bytes) coexists with MaskedMnemonicUTF8, undermining masking; change the flow so you copy the Data bytes immediately into a caller-owned inout [UInt8] buffer, pass that buffer into a new consuming initializer on MaskedMnemonicUTF8 (e.g., MaskedMnemonicUTF8.init(plaintextBytes: inout [UInt8]) or a consume method), then securely scrub the original Data-backed storage and the stack buffer on defer (overwrite with zeros via a secure memset or explicit zeroing) before returning; update the resolve code to allocate the [UInt8], copy mnemonicUTF8Bytes into it, remove reliance on mnemonicUTF8Bytes after copying, and ensure MaskedMnemonicUTF8 consumes/scrubs the buffer so no plaintext Data lives alongside the masked representation.
🧹 Nitpick comments (2)
packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift (1)
68-70: LegacytoSeed(mnemonic: String, …)still leaves the plaintext mnemonic in an unscrubbable SwiftString.This overload is preserved (and the new test exercises it), so callers that still hand in a
Stringget no obfuscation benefit from the rest of the PR — the bytes will live in the caller'sStringand in the temporaryData(mnemonic.utf8)until ARC drops them. Consider deprecating this overload (@available(*, deprecated, message: "Prefer toSeed(mnemonicUTF8Bytes:)…")) so callers migrate to the byte-based path, leaving the test the only intentional consumer.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift` around lines 68 - 70, Mark the legacy overload toSeed(mnemonic: String, passphrase: String? = nil) as deprecated so callers are encouraged to migrate to the byte-based toSeed(mnemonicUTF8Bytes: Data, passphrase: String?) variant; add an `@available`(*, deprecated, message: "Prefer toSeed(mnemonicUTF8Bytes:) to avoid keeping plaintext mnemonic in memory") annotation to the String-based method and keep its implementation unchanged so tests still pass.packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift (1)
17-61:MaskedMnemonicUTF8.initconsumes aData; consider takinginout [UInt8]to give the caller a scrubbable input buffer.
init(plaintextUTF8Bytes: Data)(Line 21) takes aData, whose backing buffer the callee cannot scrub. The localplaintextarray (Line 22) is correctly zeroed at Line 33/43, but the caller'sDatalives on. Accepting aninout [UInt8]would let the caller place the secret bytes into a buffer that can be scrubbed in adefer, removing the residual unscrubbed plaintext from this hot path. See the suggested diff in theresolve(...)comment for the matching call-site shape.Optional, but it would close the only remaining "plaintext on the Swift heap" window in this resolver.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift` around lines 17 - 61, MaskedMnemonicUTF8.init currently accepts a Data (init(plaintextUTF8Bytes: Data)) which leaves the caller's Data backing buffer uncleansed; change the initializer to accept an inout [UInt8] (e.g., init(plaintextUTF8Bytes: inout [UInt8])) so the caller can supply a scrubbable buffer, then operate directly on that buffer: generate localMask with SecRandomCopyBytes into a local array, XOR into maskedBytes, scrub and zero the input inout buffer with scrubBytes before storing maskedBytes/maskBytes, and keep deinit and withDeobfuscatedBytes (and scrubBytes calls) unchanged; update any call site that constructed a Data to instead pass a mutable [UInt8] so no plaintext remains on the Swift heap.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift`:
- Around line 125-141: hasMnemonic currently only checks SecItemCopyMatching
status and can return true for an empty stored value, causing a mismatch with
retrieveMnemonicUTF8Bytes which treats zero-length data as missing. Update
hasMnemonic(for:) to request the stored data (use kSecReturnData /
SecItemCopyMatching) and treat missing status or returned Data.isEmpty as false;
return true only when SecItemCopyMatching succeeds and the Data length > 0.
Reference functions: hasMnemonic(for:), retrieveMnemonicUTF8Bytes(for:), and
storeMnemonic(...) to ensure behavior stays consistent with how mnemonics are
stored.
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift`:
- Around line 82-87: The code appends a NUL to mnemonicBytes which may
reallocate and leave the original plaintext buffer unsanitized; before calling
mnemonicBytes.append(0) in Mnemonic.swift, reserve space to avoid reallocation
(e.g. call mnemonicBytes.reserveCapacity(mnemonicBytes.count + 1) or allocate
the array with extra capacity) so scrubMnemonicBytes(&mnemonicBytes) in the
defer will wipe the only buffer containing plaintext; apply the same reservation
pattern inside MaskedMnemonicUTF8.init (referencing the plaintext variable in
MnemonicResolverAndPersister.swift) to ensure no intermediate reallocations
leave plaintext in freed memory.
---
Outside diff comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift`:
- Around line 145-194: The stored plaintext Data (mnemonicUTF8Bytes) coexists
with MaskedMnemonicUTF8, undermining masking; change the flow so you copy the
Data bytes immediately into a caller-owned inout [UInt8] buffer, pass that
buffer into a new consuming initializer on MaskedMnemonicUTF8 (e.g.,
MaskedMnemonicUTF8.init(plaintextBytes: inout [UInt8]) or a consume method),
then securely scrub the original Data-backed storage and the stack buffer on
defer (overwrite with zeros via a secure memset or explicit zeroing) before
returning; update the resolve code to allocate the [UInt8], copy
mnemonicUTF8Bytes into it, remove reliance on mnemonicUTF8Bytes after copying,
and ensure MaskedMnemonicUTF8 consumes/scrubs the buffer so no plaintext Data
lives alongside the masked representation.
In
`@packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swift`:
- Around line 755-774: deriveAndStoreIdentityKey currently fetches
mnemonicUTF8Bytes via WalletStorage().retrieveMnemonicUTF8Bytes and calls
Mnemonic.toSeed which leaves both mnemonicUTF8Bytes and seed as plain Swift Data
on the heap; change this to avoid materializing plaintext: either (preferred)
route the whole derivation into a single Rust FFI entry (e.g., implement a Rust
function analogous to dash_sdk_sign_with_mnemonic_resolver_and_path that accepts
a MnemonicResolver + derivation path + out keychain id and performs
mnemonic→seed→identity-key derivation and storage end-to-end, then call that
from deriveAndStoreIdentityKey instead of retrieving mnemonic bytes in Swift),
or (minimal) immediately copy mnemonicUTF8Bytes and seed into [UInt8] buffers
and add a defer {} that calls memset_s to scrub both buffers before returning
(mirroring the existing privateKey scrub), and stop keeping the original Data
around; reference WalletStorage.retrieveMnemonicUTF8Bytes, Mnemonic.toSeed,
deriveAndStoreIdentityKey, MaskedMnemonicUTF8,
KeychainSigner.signPlatformAddressOnDemand and MnemonicResolver when
implementing the change.
---
Nitpick comments:
In
`@packages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swift`:
- Around line 17-61: MaskedMnemonicUTF8.init currently accepts a Data
(init(plaintextUTF8Bytes: Data)) which leaves the caller's Data backing buffer
uncleansed; change the initializer to accept an inout [UInt8] (e.g.,
init(plaintextUTF8Bytes: inout [UInt8])) so the caller can supply a scrubbable
buffer, then operate directly on that buffer: generate localMask with
SecRandomCopyBytes into a local array, XOR into maskedBytes, scrub and zero the
input inout buffer with scrubBytes before storing maskedBytes/maskBytes, and
keep deinit and withDeobfuscatedBytes (and scrubBytes calls) unchanged; update
any call site that constructed a Data to instead pass a mutable [UInt8] so no
plaintext remains on the Swift heap.
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift`:
- Around line 68-70: Mark the legacy overload toSeed(mnemonic: String,
passphrase: String? = nil) as deprecated so callers are encouraged to migrate to
the byte-based toSeed(mnemonicUTF8Bytes: Data, passphrase: String?) variant; add
an `@available`(*, deprecated, message: "Prefer toSeed(mnemonicUTF8Bytes:) to
avoid keeping plaintext mnemonic in memory") annotation to the String-based
method and keep its implementation unchanged so tests still pass.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7db9cf56-dfe6-4ab2-b871-6016382c8223
📒 Files selected for processing (6)
packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swiftpackages/swift-sdk/Sources/SwiftDashSDK/FFI/KeychainSigner.swiftpackages/swift-sdk/Sources/SwiftDashSDK/FFI/MnemonicResolverAndPersister.swiftpackages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swiftpackages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletPersistenceHandler.swiftpackages/swift-sdk/SwiftTests/Tests/SwiftDashSDKTests/PlatformWalletTests.swift
| /// Cheap existence check used by signer preflight paths. | ||
| /// | ||
| /// Unlike `retrieveMnemonic(...)`, this does not materialize the | ||
| /// mnemonic bytes into Swift heap objects. | ||
| public func hasMnemonic(for walletId: Data) -> Bool { | ||
| let account = perWalletMnemonicAccount(for: walletId) | ||
| let query: [String: Any] = [ | ||
| kSecClass as String: kSecClassGenericPassword, | ||
| kSecAttrService as String: keychainService, | ||
| kSecAttrAccount as String: account, | ||
| kSecMatchLimit as String: kSecMatchLimitOne, | ||
| kSecReturnAttributes as String: true | ||
| ] | ||
| var result: AnyObject? | ||
| let status = SecItemCopyMatching(query as CFDictionary, &result) | ||
| return status == errSecSuccess | ||
| } |
There was a problem hiding this comment.
Preflight hasMnemonic and runtime retrieveMnemonicUTF8Bytes disagree on the "empty stored data" case.
retrieveMnemonicUTF8Bytes(for:) throws mnemonicNotFound not just on errSecItemNotFound but also when the keychain returns a Data whose isEmpty is true (Lines 110-112). hasMnemonic(for:) only checks status == errSecSuccess (Line 140) — it doesn't request the bytes and doesn't validate any size attribute. So a corrupt-or-zero-byte keychain row makes hasMnemonic return true while the actual signing path then fails with mnemonicNotFound, which surfaces in KeychainSigner.canSign as a "yes I can sign" lie followed by a sign-time error.
Two reasonable fixes:
- Single source of truth, no plaintext materialized. Inspect the persistent-data attribute size during the existence check (e.g., include
kSecReturnAttributeswithkSecAttrSize as Stringor usekSecReturnPersistentRef+ a follow-up data length query) and treat size==0 as "no mnemonic". This keepshasMnemonicplaintext-free. - Fetch the bytes anyway and zero-check. Pragmatically simpler — it does materialize the
Data, but only on the rare preflight callers, and it removes the disagreement entirely.
Empty rows shouldn't appear in practice (storeMnemonic writes Data(mnemonic.utf8) from a non-empty source), but since retrieveMnemonicUTF8Bytes actively guards the case, the two APIs should agree on what "has" means.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/swift-sdk/Sources/SwiftDashSDK/Core/Wallet/WalletStorage.swift`
around lines 125 - 141, hasMnemonic currently only checks SecItemCopyMatching
status and can return true for an empty stored value, causing a mismatch with
retrieveMnemonicUTF8Bytes which treats zero-length data as missing. Update
hasMnemonic(for:) to request the stored data (use kSecReturnData /
SecItemCopyMatching) and treat missing status or returned Data.isEmpty as false;
return true only when SecItemCopyMatching succeeds and the Data length > 0.
Reference functions: hasMnemonic(for:), retrieveMnemonicUTF8Bytes(for:), and
storeMnemonic(...) to ensure behavior stays consistent with how mnemonics are
stored.
| var mnemonicBytes = [UInt8](mnemonicUTF8Bytes) | ||
| guard !mnemonicBytes.contains(0) else { | ||
| scrubMnemonicBytes(&mnemonicBytes) | ||
| throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL") | ||
| } | ||
| mnemonicBytes.append(0) |
There was a problem hiding this comment.
mnemonicBytes.append(0) may reallocate, leaving the prior plaintext buffer un-scrubbed.
var mnemonicBytes = [UInt8](mnemonicUTF8Bytes) (Line 82) initializes the array at exact size, so capacity == count. The subsequent mnemonicBytes.append(0) (Line 87) will then trigger a reallocation: Swift grows the storage, copies the plaintext into the new buffer, and frees the old one without zeroing it. The defer { scrubMnemonicBytes(&mnemonicBytes) } at Line 111 only scrubs the current (post-realloc) buffer; the original buffer with the plaintext mnemonic is returned to the allocator dirty.
Reserve the final capacity up front so no reallocation occurs:
🔒 Proposed fix
- var mnemonicBytes = [UInt8](mnemonicUTF8Bytes)
- guard !mnemonicBytes.contains(0) else {
- scrubMnemonicBytes(&mnemonicBytes)
- throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL")
- }
- mnemonicBytes.append(0)
+ var mnemonicBytes = [UInt8]()
+ mnemonicBytes.reserveCapacity(mnemonicUTF8Bytes.count + 1)
+ mnemonicBytes.append(contentsOf: mnemonicUTF8Bytes)
+ guard !mnemonicBytes.contains(0) else {
+ scrubMnemonicBytes(&mnemonicBytes)
+ throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL")
+ }
+ mnemonicBytes.append(0)The same pattern would also be worth applying inside MaskedMnemonicUTF8.init (MnemonicResolverAndPersister.swift), where var plaintext = [UInt8](plaintextUTF8Bytes) is read-only after init and so does not hit this pitfall — but it's a useful invariant to keep consistent.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var mnemonicBytes = [UInt8](mnemonicUTF8Bytes) | |
| guard !mnemonicBytes.contains(0) else { | |
| scrubMnemonicBytes(&mnemonicBytes) | |
| throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL") | |
| } | |
| mnemonicBytes.append(0) | |
| var mnemonicBytes = [UInt8]() | |
| mnemonicBytes.reserveCapacity(mnemonicUTF8Bytes.count + 1) | |
| mnemonicBytes.append(contentsOf: mnemonicUTF8Bytes) | |
| guard !mnemonicBytes.contains(0) else { | |
| scrubMnemonicBytes(&mnemonicBytes) | |
| throw KeyWalletError.invalidInput("Mnemonic bytes must not contain NUL") | |
| } | |
| mnemonicBytes.append(0) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/swift-sdk/Sources/SwiftDashSDK/KeyWallet/Mnemonic.swift` around
lines 82 - 87, The code appends a NUL to mnemonicBytes which may reallocate and
leave the original plaintext buffer unsanitized; before calling
mnemonicBytes.append(0) in Mnemonic.swift, reserve space to avoid reallocation
(e.g. call mnemonicBytes.reserveCapacity(mnemonicBytes.count + 1) or allocate
the array with extra capacity) so scrubMnemonicBytes(&mnemonicBytes) in the
defer will wipe the only buffer containing plaintext; apply the same reservation
pattern inside MaskedMnemonicUTF8.init (referencing the plaintext variable in
MnemonicResolverAndPersister.swift) to ensure no intermediate reallocations
leave plaintext in freed memory.
|
✅ DashSDKFFI.xcframework built for this PR.
SwiftPM (host the zip at a stable URL, then use): .binaryTarget(
name: "DashSDKFFI",
url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
checksum: "27428b3467ae300cd12f7399717509aece2c68a3a60546b025dd1c994f49b8a2"
)Xcode manual integration:
|
b44bcfb to
29bbe7a
Compare
llbartekll
left a comment
There was a problem hiding this comment.
Looks good to me, useful incremental security improvement!
Summary
Testing
Summary by CodeRabbit
New Features
Improvements
Tests