Proposal: Add Secure Memory Management API for Sensitive Data in Swift

Hi,

Swift currently lacks a built-in secure memory abstraction similar to C#'s SecureString for handling sensitive data like passwords, API keys, and cryptographic secrets. This forces developers to either:

  1. Use Swift's String type - which is insecure due to copy-on-write optimization and potential string interning, leaving copies of sensitive data scattered in memory
  2. Manually manage unsafe buffers - which is error-prone and requires a deep understanding of memory management.

I'm not an experienced Swift developer and perhaps I am missing something obvious, but I reviewed the SWIFT documentation, particularly memorysafety, and could not find the answer to my question.

Demonstration of the Problem

The following code demonstrates the security vulnerability:

import Foundation

print("Enter password: ", terminator: "")
if let cString = getpass("") {
    let password = String(cString: cString)
    print("\nPassword length: \(password.count)")

    // Zero out the buffer after use
    memset(UnsafeMutableRawPointer(mutating: cString), 0, strlen(cString))

    print("Press Enter to exit...")
    _ = readLine()
}

Memory analysis reveals the password string persists in memory even after explicit clearing attempts.

My Workaround

Developers must currently implement manual unsafe buffer management:
(Here should be an image which shows no leak, but as I am a new user I receive : An error occurred: Sorry, new users can only put one embedded media item in a post. so you have to trust me, or test it yourself :smiley:)

import Foundation

print("Enter password: ", terminator: "")
if let cString = getpass("") {
    let length = strlen(cString)
    // Allocate a buffer
    let buffer = UnsafeMutablePointer<CChar>.allocate(capacity: length + 1)
    buffer.initialize(from: cString, count: length + 1)
    
    // ... use `buffer` for authentication, etc. ...

    // Zero out the buffer after use
    memset(buffer, 0, length + 1)
    buffer.deallocate()
    // Also zero out the original cString from getpass
    memset(UnsafeMutableRawPointer(mutating: cString), 0, length)

    print("Press Enter to exit...")
    _ = readLine()
}

This approach is error-prone and requires specialized knowledge, creating barriers to secure development. Manual buffer management increases bug risk. I believe there should be an API for that.

Conclusion

This has a global impact on Apple's application security ecosystem. I have tested applications written in Swift, all of which leak credentials, API keys, and other sensitive data. macOS provides default protection against extracting sensitive data left in memory through a hardened runtime feature that prevents task_for_pid(). However, developers do not always enable the hardened runtime for their applications. Furthermore, this protection is not foolproof and can be bypassed or misconfigured. Additionally, the app's memory can also be stored in core dumps, swap files, hibernation files, and accessed by security scanners with elevated permissions. Currently, it seems that there are mitigations in place that make it harder to read the process's memory, but there are no solutions for securely storing and clearing sensitive data in memory.

10 Likes

Hi @Karmaz95

This is a type of API that we're definitely interested in working on, but I want to point out that the example of the need here belies some fundamental misunderstandings about the existing APIs that would be useful to correct. In particular, this line:

memset(UnsafeMutableRawPointer(mutating: cString), 0, strlen(cString))

attempts to zero the memory pointed to by cString, but is not guaranteed to do so (and in general would not do so, even in C or C++) because the memory is non-volatile and is never referenced after the memset, so the operation is statically dead in the C[++] (and also Swift) memory model. In C-family languages you would use an API like memset_s to guarantee zeroing happens.

More importantly, while this line zeros cString, it doesn't even try to zero the memory holding the Swift String itself (String.init(cString:) copies the contents of its argument, because a native Swift string always owns the memory backing its contents). This would also be an issue with similar API in any other language.

Now, on to the Swift-specific aspects of the issue you raise. Because String is copyable in Swift, the compiler can in general introduce arbitrary temporary copies of the object such that even if you explicitly zero the bound object itself, those copies may not be zeroed.

Swift introduced the ~Copyable constraint last year, which makes it possible to define a type that cannot be implicitly copied. This opens the door to a partial mitigation for the identified issue. Defining a non-copyable String type is pretty easy. The problem you then face is that you can't do anything with one, because all the standard library and system API and third-party frameworks expect a normal String, and you have the wrong type. Some of those APIs would have to be extended to work with that new "SecureString" type, but for others it probably makes more sense for them to adopt UTF8Span (which is implicitly copyable, but non-escapable, and copying a UTF8Span doesn't copy its contents because it is fundamentally a reference type), which would allow them to work with both String and "SecureString". Expanding all those API to work with the new type is a large project (worth doing, but quite significant).

Finally, I want to emphasize that this will at best only ever be a partial mitigation. Even though we can make a SecureString that cannot be copied and is self-zeroing at the end of its lifetime, fragments of the string may persist in registers or in memory where registers were spilled by function calls for an arbitrarily long period after the string's lifetime has ended. A complete solution is plausible, but would require deep compiler integration that I don't think any mainstream language or library has actually tackled with their "secure string" APIs.¹ On some platforms, some of the low-level string handling routines like memcpy deliberately zero-out registers after use to prevent this, but this is generally not the case across platforms, nor is it an implementation detail that Swift or any other language can depend on.

I would also note that your manual workaround can be made much, much safer in the Swift we have today; there's no need to go to fully-unchecked API just to use some manual memory management. Perhaps @glessard can write some notes on a safer approach.

tl;dr: yes, we should do this, but it's a larger project than it seems like at first, and it cannot offer complete safety, so documenting its limitations carefully is really vital (I really wish that existing solutions in other languages did a better job of being upfront about the limitations they have).


¹ This is also very much a project worth doing, but mostly broader in scope than Swift Evolution.

19 Likes

Steve's already said all of the important things. I'm just here for extra flavor.

The plan for strict memory safety is to prevent memory corruption in your process. Another process being able to access your memory is out of scope for it. That's why this specific bit is not addressed by the strict memory safety project.

It could be easier to do this right, but it will never be easy to do this right. In languages that have some variant of it, SecureString is frequently used wrong. The pipelines for secure and insecure strings are vastly shared in a way that does not assume secrecy, especially at UI layers. For instance, getpass() has ambiguous ownership and secrecy properties that are more suitable to an insecure string than a secure one; for all we know, it stashed a copy of the password somewhere else already. An AppKit/UIKit password input field gives you back a NSString; that string can be zeroing on deinit, but:

  • bridging to Swift can lose that property;
  • the fact it's refcounted means you do not actually know when it's being deleted.

Lastly, memset_s is effective when you know the memory being cleared is actual memory and not, like, bits held in registers; it is actively harmful otherwise, as it can cause secrets held in registers to be spilled to memory for the sole purpose of zeroing that memory. Doing this right, like Steve said, is a project that needs cooperation between LLVM back-ends and language front-ends and API developers.

8 Likes