1 Getting Started
This guide walks through common usage patterns for the DPAPI library.
1.1 Installation
Until Racket is updated to support SHA256 commits, you will need to clone the source code and then install locally with:
git clone https://codeberg.org/joeld/racket-dpapi.git |
cd racket-dpapi |
raco pkg install dpapi/ dpapi-lib/ |
Omit dpapi/ to exclude building the local documentation.
Then require it in your Racket programs:
(require dpapi)
1.2 Protecting Data in Memory
The core abstraction is the protected value, which keeps sensitive data encrypted in memory. The only way to access the plaintext is through a callback inside with-decrypted-data:
(require dpapi) ;; Check if DPAPI is available (unless (dpapi-available?) (error "DPAPI not available on this platform")) ;; Create a protected value - data is encrypted immediately (define password (make-protected-value (string->bytes/utf-8 "my-secret-password"))) ;; Access the data through a callback (with-decrypted-data password (lambda (pwd-bytes) (define pwd (bytes->string/utf-8 pwd-bytes)) (connect-to-database pwd))) ; Password is automatically re-encrypted after the callback ; When done, zero out memory (destroy-protected-value! password)
The with-decrypted-data pattern ensures that data remains unencrypted only during the time it is needed. The data gets re-encrypted automatically once the callback is complete, even if the callback throws an exception.
1.3 Saving and Loading Encrypted Data
A protected value is not suitable for writing to permanent storage, because it is generally scoped only to the currently running process.
The export-protected-bytes and import-protected-bytes functions convert between in-memory protected values and DPAPI-encrypted values that can be used across sessions, without exposing the plaintext to the rest of your code:
(require dpapi) ;; Create a protected value (define token (make-protected-value (string->bytes/utf-8 token-from-oauth))) ;; Export to DPAPI-encrypted bytes and save to disk (define encrypted-bytes (export-protected-bytes token #:entropy (string->bytes/utf-8 "app-v1-secret") #:description "OAuth Token")) (with-output-to-file "token.encrypted" (lambda () (write-bytes encrypted-bytes)) #:exists 'replace) ;; --- Later: load from disk --- (define loaded-token (import-protected-bytes (file->bytes "token.encrypted") #:entropy (string->bytes/utf-8 "app-v1-secret"))) ;; Use the token through a callback (with-decrypted-data loaded-token (lambda (token-bytes) (make-api-request (bytes->string/utf-8 token-bytes))))
To retrieve the description stored with the encrypted data:
(define loaded-token (import-protected-bytes (file->bytes "token.encrypted") #:entropy (string->bytes/utf-8 "app-v1-secret"))) (protected-value-description loaded-token) ;; => "OAuth Token"
1.3.1 Entropy
The optional #:entropy parameter on export-protected-bytes and import-protected-bytes adds a secondary secret. Without the correct entropy, decryption will fail even for the same Windows user. The same entropy must be provided for both export and import.
; Export with entropy (define encrypted (export-protected-bytes pv #:entropy (string->bytes/utf-8 "my-app-secret"))) ;; Import must use the same entropy (define pv2 (import-protected-bytes encrypted #:entropy (string->bytes/utf-8 "my-app-secret"))) ;; Wrong entropy will raise exn:fail:dpapi
1.4 Complete Example: Storing Configuration
Here’s a complete example for storing encrypted configuration:
(require dpapi json) ;; Configuration to encrypt (define config (hasheq 'database-password "super-secret" 'api-key "key-12345")) ;; Serialize, protect, and export (define config-json (jsexpr->string config)) (define config-pv (make-protected-value (string->bytes/utf-8 config-json))) (define encrypted-config (export-protected-bytes config-pv #:description "App Configuration" #:entropy (string->bytes/utf-8 "app-v1-secret"))) ;; Save to file (with-output-to-file "config.encrypted" (lambda () (write-bytes encrypted-config)) #:exists 'replace) (destroy-protected-value! config-pv) ;; --- Later: Load and use --- ;; Import from file (define loaded-config-pv (import-protected-bytes (file->bytes "config.encrypted") #:entropy (string->bytes/utf-8 "app-v1-secret"))) ;; Use the config through a callback (with-decrypted-data loaded-config-pv (lambda (config-bytes) (define loaded-config (string->jsexpr (bytes->string/utf-8 config-bytes))) (displayln loaded-config))) ; => '#hasheq((database-password . "super-secret") (api-key . "key-12345"))