Skip to content

Encrypt bytes with CryptProtectData

This is the lowest-level encrypt operation: it marshals plaintext and entropy byte arrays into unmanaged memory, invokes the native crypt32!CryptProtectData DPAPI primitive, and copies the resulting ciphertext back into managed memory, always freeing the unmanaged buffers regardless of outcome.

Field Value
Operation ID OP-04
Trigger Called by Encrypt a string with DPAPI step 4
Inputs key scope, plaintext bytes, entropy bytes, description string
Outputs Ciphertext byte array
Primary components DPAPI.Encrypt(KeyType, byte[], byte[], string)
Entry point src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs

Steps

  1. Coerce null plaintext, entropy, and description arguments to empty values (BR-08)
  2. Initialize three DATA_BLOBs and a CRYPTPROTECT_PROMPTSTRUCT with no prompt text and no window handle (BR-11)
  3. Copy the plaintext into unmanaged memory using Marshal.AllocHGlobal and Marshal.Copy.
  4. On failure: throws Exception("Cannot initialize plaintext BLOB."), rewrapped by step 9.
  5. Copy the entropy into unmanaged memory the same way.
  6. On failure: throws Exception("Cannot initialize entropy BLOB."), rewrapped by step 9.
  7. Compute the native flags: always CRYPTPROTECT_UI_FORBIDDEN (1); add CRYPTPROTECT_LOCAL_MACHINE (4) when the key scope is MachineKey (BR-04, BR-11)
  8. UserKey produces flags 1; MachineKey produces flags 5.
  9. Call the native crypt32!CryptProtectData primitive.
  10. On failure: on a non-Windows host, or a Windows host without crypt32.dll at c:\windows\system32\crypt32.dll, the native call throws DllNotFoundException. This call is not inside a nested try/catch of its own, so the exception propagates directly to the outer catch in step 9, which rewraps it as Exception("DPAPI was unable to encrypt data.", innerException) with the DllNotFoundException as the inner cause (BR-13, BR-12).
  11. If the call returns false, read GetLastWin32Error and throw Exception("CryptProtectData fialed.", Win32Exception) (BR-12)
  12. Rewrapped by step 9.
  13. Copy the resulting ciphertext bytes out to a managed array and return them.
  14. On any exception from steps 3–8, throw Exception("DPAPI was unable to encrypt data.", innerException) (BR-12)
  15. All failures surface to the caller as a generic System.Exception.
  16. Always free the three unmanaged buffers in a finally block when their handles are non-zero.

Process flow

The following diagram visualizes this operation's control flow, including its always-run cleanup path.

flowchart TD
    Start(["Trigger: Encrypt bytes called"]) --> S1["Coerce null args<br>to empty values"]
    S1 --> S2["Initialize DATA_BLOBs<br>and prompt struct"]
    S2 --> S3["Copy plaintext to<br>unmanaged memory"]
    S3 --> D1{Copy failed?}
    D1 -->|Yes| ErrWrap
    D1 -->|No| S4["Copy entropy to<br>unmanaged memory"]
    S4 --> D2{Copy failed?}
    D2 -->|Yes| ErrWrap
    D2 -->|No| S5["Compute flags:<br>UI_FORBIDDEN + LOCAL_MACHINE if applicable"]
    S5 --> S6["Call CryptProtectData<br>(native)"]
    S6 --> D3{Non-Windows host or<br>DLL not found?}
    D3 -->|Yes: DllNotFoundException| ErrWrap
    D3 -->|No| D4{Returned false?}
    D4 -->|Yes| ErrWrap
    D4 -->|No| S8["Copy ciphertext bytes<br>to managed array"]
    S8 --> Cleanup
    ErrWrap["Throw wrapped Exception:<br>unable to encrypt data<br>(inner exception preserved)"] --> Cleanup["Free unmanaged<br>buffers in finally"]
    Cleanup --> D5{Exception pending?}
    D5 -->|Yes| EndErr(["Exception propagated"])
    D5 -->|No| EndOk([Ciphertext bytes returned])

Failure and edge-case behavior

  • A non-Windows host, or a Windows host without crypt32.dll at c:\windows\system32\crypt32.dll, causes the native call to throw DllNotFoundException; this is caught by the outer catch and rewrapped as the same generic System.Exception("DPAPI was unable to encrypt data.") as every other failure path, with the DllNotFoundException preserved as the inner exception — it is not thrown directly to the caller (BR-13).
  • Within this operation's try block (steps 3–8), every failure path — unmanaged memory initialization, a native DLL load failure, or a false return from CryptProtectData — is rewrapped as a single generic System.Exception("DPAPI was unable to encrypt data."), so callers cannot catch a specific exception type; the original cause (including the Win32 error code for a false return) is only available in the inner exception (BR-12). Null-argument coercion (step 1) and struct initialization (step 2), which run before the try block, cannot fail and are therefore outside this guarantee's scope.
  • Unmanaged buffers are always freed in a finally block regardless of success or failure, preventing memory leaks across repeated calls.

Sources

  • src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs