1 Getting Started
This guide walks through common usage patterns for the DPAPI library.
1.1 Installation
Install from the Racket package catalog:
raco pkg install dpapi |
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. Creating one encrypts a copy of the bytes you pass in; the result is an opaque value that does not reveal its contents when printed:
> (require dpapi) > (dpapi-available?) #t
> (define password (make-protected-value (string->bytes/utf-8 "my-secret-password"))) > password #<protected-value>
The only way to access the plaintext is through a callback passed to with-decrypted-data. The callback receives the decrypted bytes, and the value is re-encrypted as soon as the callback returns, even if it raises an exception. In practice the callback would do the real work, such as opening a database connection, and return only what the rest of the program needs:
> (with-decrypted-data password (lambda (pwd-bytes) (bytes-length pwd-bytes))) 18
When a protected value is no longer needed, destroy it. This zeros the encrypted buffer, and any later attempt to use the value raises an error:
> (destroy-protected-value! password)
> (with-decrypted-data password (lambda (pwd-bytes) (bytes-length pwd-bytes))) with-decrypted-data: protected value has been destroyed
Note that make-protected-value encrypts a copy of the bytes you pass in. The original byte string is left untouched, so in the example above the plaintext password also remains in memory until it is garbage collected. See Practical Guidance for how to zero it yourself.
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. The exported bytes are an opaque DPAPI blob that can be written to disk as-is:
> (require racket/file) > (define token (make-protected-value #"oauth-token-value"))
> (define encrypted-bytes (export-protected-bytes token #:entropy (string->bytes/utf-8 "app-v1-secret") #:description "OAuth Token")) > encrypted-bytes #"\1\0\0\0\320\214\235\337\1\25\321\21\214z\0\300O\302\227\353\1\0\0\0"…
> (bytes-length encrypted-bytes) 268
> (display-to-file encrypted-bytes "token.encrypted" #:exists 'replace)
Later, possibly in another process, read the file back and import it. The result is a new protected value, and the description stored alongside the encrypted data is available through protected-value-description:
> (define loaded-token (import-protected-bytes (file->bytes "token.encrypted") #:entropy (string->bytes/utf-8 "app-v1-secret"))) > loaded-token #<protected-value>
> (protected-value-description loaded-token) "OAuth Token"
> (with-decrypted-data loaded-token (lambda (token-bytes) (bytes-length token-bytes))) 17
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. A mismatch, or omitting the entropy on import, raises exn:fail:dpapi:
> (import-protected-bytes (file->bytes "token.encrypted") #:entropy (string->bytes/utf-8 "wrong-secret")) CryptUnprotectData failed: ERROR_INVALID_DATA: The data is invalid or corrupted
> (import-protected-bytes (file->bytes "token.encrypted")) CryptUnprotectData failed: ERROR_INVALID_DATA: The data is invalid or corrupted
1.4 Complete Example: Storing Configuration
Here is a complete example for storing encrypted configuration. The configuration is serialized to JSON, protected, exported to a file, and the in-memory copy is destroyed:
> (require json)
> (define config (hasheq 'database-password "super-secret" 'api-key "key-12345"))
> (define config-pv (make-protected-value (string->bytes/utf-8 (jsexpr->string config))))
> (display-to-file (export-protected-bytes config-pv #:description "App Configuration" #:entropy (string->bytes/utf-8 "app-v1-secret")) "config.encrypted" #:exists 'replace) > (destroy-protected-value! config-pv)
Later, the file is imported and the configuration is parsed inside the callback:
> (define loaded-config-pv (import-protected-bytes (file->bytes "config.encrypted") #:entropy (string->bytes/utf-8 "app-v1-secret"))) > (protected-value-description loaded-config-pv) "App Configuration"
> (with-decrypted-data loaded-config-pv (lambda (config-bytes) (string->jsexpr (bytes->string/utf-8 config-bytes)))) '#hasheq((api-key . "key-12345") (database-password . "super-secret"))