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¶
- Coerce
nullplaintext, entropy, and description arguments to empty values (BR-08) - Initialize three
DATA_BLOBs and aCRYPTPROTECT_PROMPTSTRUCTwith no prompt text and no window handle (BR-11) - Copy the plaintext into unmanaged memory using
Marshal.AllocHGlobalandMarshal.Copy. - On failure: throws
Exception("Cannot initialize plaintext BLOB."), rewrapped by step 9. - Copy the entropy into unmanaged memory the same way.
- On failure: throws
Exception("Cannot initialize entropy BLOB."), rewrapped by step 9. - Compute the native flags: always
CRYPTPROTECT_UI_FORBIDDEN (1); addCRYPTPROTECT_LOCAL_MACHINE (4)when the key scope isMachineKey(BR-04, BR-11) UserKeyproduces flags1;MachineKeyproduces flags5.- Call the native
crypt32!CryptProtectDataprimitive. - On failure: on a non-Windows host, or a Windows host without
crypt32.dllatc:\windows\system32\crypt32.dll, the native call throwsDllNotFoundException. This call is not inside a nestedtry/catchof its own, so the exception propagates directly to the outercatchin step 9, which rewraps it asException("DPAPI was unable to encrypt data.", innerException)with theDllNotFoundExceptionas the inner cause (BR-13, BR-12). - If the call returns
false, readGetLastWin32Errorand throwException("CryptProtectData fialed.", Win32Exception)(BR-12) - Rewrapped by step 9.
- Copy the resulting ciphertext bytes out to a managed array and return them.
- On any exception from steps 3–8, throw
Exception("DPAPI was unable to encrypt data.", innerException)(BR-12) - All failures surface to the caller as a generic
System.Exception. - Always free the three unmanaged buffers in a
finallyblock 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.dllatc:\windows\system32\crypt32.dll, causes the native call to throwDllNotFoundException; this is caught by the outercatchand rewrapped as the same genericSystem.Exception("DPAPI was unable to encrypt data.")as every other failure path, with theDllNotFoundExceptionpreserved 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
falsereturn fromCryptProtectData— is rewrapped as a single genericSystem.Exception("DPAPI was unable to encrypt data."), so callers cannot catch a specific exception type; the original cause (including the Win32 error code for afalsereturn) 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
finallyblock regardless of success or failure, preventing memory leaks across repeated calls.
Related documentation¶
Sources¶
src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs