On this page:
2.1 Platform Detection
dpapi-available?
2.2 Protected Values
make-protected-value
with-decrypted-data
destroy-protected-value!
protected-value?
protected-value-description
2.3 Disk Persistence
export-protected-bytes
import-protected-bytes
2.4 Error Handling
exn:  fail:  dpapi
9.3

2 API Reference🔗

2.1 Platform Detection🔗

procedure

(dpapi-available?) → boolean?

Returns #t if DPAPI is available on the current platform, #f otherwise.

On Windows Vista or later, this returns #t. On all other platforms (Linux, macOS, etc.), it returns #f.

2.2 Protected Values🔗

A protected value is an opaque wrapper that keeps sensitive data encrypted in memory using CryptProtectMemory. The only way to access the plaintext is through with-decrypted-data, which temporarily decrypts the data for the duration of a callback and automatically re-encrypts afterward.

procedure

(make-protected-value data 
  [#:scope scope 
  #:description description]) 
 → protected-value?
  data : bytes?
  scope : (or/c 'same-process 'cross-process 'same-logon)
   = 'same-process
  description : (or/c string? #f) = #f
Creates a protected value with data immediately encrypted in memory.

The data is automatically padded to 16-byte blocks and encrypted. Returns an opaque struct that can only be accessed through with-decrypted-data.

The scope parameter controls which processes can decrypt the in-memory representation:

  • 'same-process: Only this process can decrypt

  • 'cross-process: Any process on this machine can decrypt

  • 'same-logon: Any process by the same logged-in user can decrypt

The description is an optional label stored in the protected value. It is used as the default description when exporting with export-protected-bytes, and can be retrieved with protected-value-description.

The contents of data are copied into the protected value; the original byte string is neither modified nor zeroed, since it may be immutable. See Practical Guidance for how to handle the original.

Example:

(define password
  (make-protected-value (string->bytes/utf-8 "secret")
                        #:description "User Password"))

procedure

(with-decrypted-data pv proc) → any

  pv : protected-value?
  proc : (-> bytes? any)
Temporarily decrypts the bytes inside pv, calls proc with the decrypted (unpadded) data, then automatically re-encrypts before returning.

The data is only exposed during the dynamic extent of the callback. The protected value is always re-encrypted, even if proc throws an exception.

This is the only way to access the data inside a protected value. Raises exn:fail if pv has been destroyed with destroy-protected-value!.

Access to a protected value is exclusive and not re-entrant. While proc is running, other threads that call with-decrypted-data, export-protected-bytes, or destroy-protected-value! on the same value block until proc returns. Calling any of those on the same value from within proc on the same thread raises exn:fail rather than deadlocking.

If the thread running proc is terminated with kill-thread, the cleanup that re-encrypts and zeros the data does not run. The plaintext remains in memory, and the value stays locked, so any later call on it blocks indefinitely (a blocked caller can still be interrupted with break-thread). Use break-thread rather than kill-thread to interrupt a callback; breaks unwind normally and the value is re-encrypted.

Example:

(with-decrypted-data password
  (lambda (pwd-bytes)
    (connect-to-database (bytes->string/utf-8 pwd-bytes))))

procedure

(destroy-protected-value! pv) → void?

  pv : protected-value?
Zeros out the encrypted data and marks the protected value as destroyed. Any subsequent call to with-decrypted-data on this value will raise exn:fail.

This is a best-effort attempt to remove sensitive data from memory. Racket’s garbage collector may have already copied the data elsewhere.

Example:

(destroy-protected-value! password)

procedure

(protected-value? v) → boolean?

  v : any/c
Returns #t if v is a protected value, #f otherwise.

procedure

(protected-value-description pv) → (or/c string? #f)

  pv : protected-value?
Returns the description associated with pv, or #f if none was provided.

When a protected value is created by import-protected-bytes, the description stored in the DPAPI blob is automatically captured here.

2.3 Disk Persistence🔗

These functions bridge between protected values (in-memory) and DPAPI-encrypted bytes suitable for writing to disk, using CryptProtectData and CryptUnprotectData internally. The plaintext is never exposed to calling code—it is decrypted and re-encrypted internally.

procedure

(export-protected-bytes pv 
  [#:description description 
  #:entropy entropy 
  #:machine-scope? machine-scope? 
  #:audit? audit?]) 
 → bytes?
  pv : protected-value?
  description : (or/c string? #f)
   = (protected-value-description pv)
  entropy : (or/c bytes? #f) = #f
  machine-scope? : boolean? = #f
  audit? : boolean? = #f
Exports the contents of pv as DPAPI-encrypted bytes suitable for persistent storage (files, databases, etc.). The returned bytes are encrypted using Windows credentials and can be imported back with import-protected-bytes. Raises exn:fail:dpapi if encryption fails, and exn:fail if DPAPI is not available or the protected value has been destroyed.

If description is not provided, the description from pv (see protected-value-description) is used. If explicitly provided, it overrides the protected value’s description. Either way, a non-#f description is stored unencrypted alongside the encrypted data, where it will be captured by import-protected-bytes.

If entropy is not #f, it is used as a factor in the encryption of the exported bytes. An attempt to re-import the resulting bytes without identical entropy will fail with an exception.

If machine-scope? is #t, the resulting bytes will be decryptable by other users on the same machine.

If audit? is #t, Windows will generate audit log entries for the operation (but only if description is provided as a non-empty string).

Example:

(define encrypted-bytes
  (export-protected-bytes password
                          #:entropy (string->bytes/utf-8 "app-secret")
                          #:description "User Password"))
 
;; Write to disk
(with-output-to-file "password.encrypted"
  (lambda () (write-bytes encrypted-bytes))
  #:exists 'replace)

procedure

(import-protected-bytes encrypted-data    
  [#:entropy entropy    
  #:scope scope]) → protected-value?
  encrypted-data : bytes?
  entropy : (or/c bytes? #f) = #f
  scope : (or/c 'same-process 'cross-process 'same-logon)
   = 'same-process
Imports DPAPI-encrypted bytes (previously created by export-protected-bytes) and returns a protected value. The plaintext is decrypted from the DPAPI format and immediately re-encrypted in memory—it is never returned directly. Raises exn:fail:dpapi if decryption fails (wrong entropy, corrupted data, wrong user/machine, etc.).

If a description was stored in the DPAPI blob, it is automatically captured in the returned protected value and can be retrieved with protected-value-description.

The entropy must match the value used during export.

The scope parameter sets the memory protection scope for the resulting protected value (see make-protected-value).

; Read from disk
(define encrypted-bytes (file->bytes "password.encrypted"))
 
; Import into a protected value
(define password
  (import-protected-bytes encrypted-bytes
                          #:entropy (string->bytes/utf-8 "app-secret")))
 
; Retrieve the description
(protected-value-description password)

2.4 Error Handling🔗

struct

(struct exn:fail:dpapi (message
    continuation-marks
    error-code
    error-message)
    #:transparent)
  message : string?
  continuation-marks : continuation-mark-set?
  error-code : exact-nonnegative-integer?
  error-message : string?
Exception structure for DPAPI errors. Extends exn:fail. This exception is raised only when a Windows DPAPI call reports failure; local validation failures (DPAPI not available, destroyed protected value, malformed data) raise a plain exn:fail instead.

  • error-code: Windows error code from GetLastError

  • error-message: Human-readable description of the error

The message field contains both context and the error message.

Error Code

  

Description

13

  

ERROR_INVALID_DATA: Data is corrupted or invalid

14

  

ERROR_OUTOFMEMORY: Not enough memory

50

  

ERROR_NOT_SUPPORTED: Operation not supported on this OS

87

  

ERROR_INVALID_PARAMETER: Invalid parameter

0x80090003

  

NTE_BAD_KEY: Wrong entropy or protection scope

0x80090005

  

NTE_BAD_DATA: Invalid encrypted data format