Skip to content

Decrypt (internal only)

DPAPI.Decrypt mirrors the encrypt overloads in reverse, but all three overloads are declared internal and have no caller anywhere in this library, and there is no InternalsVisibleTo declaration, so this operation is dead code from any consumer's point of view. It is documented here because it is part of the compiled surface, and because the public CryptProtectData/CryptUnprotectData externs mean the underlying decrypt capability is still reachable outside this managed wrapper — see Security and authorization.

Field Value
Operation ID OP-06
Trigger None — no member of the library calls Decrypt; recorded for completeness
Inputs Base64 string or raw ciphertext bytes, and entropy (string or bytes)
Outputs Decrypted plaintext as the return value (string for the string overloads, byte[] for the byte-array overload); the description ref parameter carries separate DPAPI description metadata, not the plaintext. Unreachable in practice.
Primary components DPAPI.Decrypt(string, ref string), Decrypt(string, string, ref string), Decrypt(byte[], byte[], ref string) — all internal
Entry point src/SAIF.NetCore.LegacyAuth/src/SAIF.NetCore.LegacyAuth/SAIF.NetCore.LegacyAuth/DPAPI.cs

Steps

  1. (String overload only) UTF-8-encode the entropy string into bytes — this happens before the ciphertext is Base64-decoded.
  2. (String overload only) Base64-decode the cipherText string into raw ciphertext bytes via Convert.FromBase64String.
  3. On failure: invalid Base64 input throws FormatException directly from Convert.FromBase64String, before the byte-array overload (and its exception wrapping) is ever reached — this failure is not wrapped as Exception("DPAPI was unable to decrypt data.").
  4. Call the byte-array overload Decrypt(byte[] cipherTextBytes, byte[] entropyBytes, ref string description), which first calls InitPrompt to populate the prompt struct, then resets the description ref parameter to "" — both before its try block starts.
  5. Inside the try block, initialize the ciphertext and entropy DATA_BLOBs (BR-11), then call the native crypt32!CryptUnprotectData primitive with CRYPTPROTECT_UI_FORBIDDEN set, passing description by reference so the native call can populate it with the DPAPI-recovered description metadata.
  6. On failure: wrapped as Exception("DPAPI was unable to decrypt data."), mirroring the encrypt path's generic wrapping behavior (BR-12).
  7. On success, copy the decrypted bytes into a managed byte[]. A finally block frees the unmanaged memory allocated for all three DATA_BLOBs before the return value or the exception propagates to the caller. The string overloads additionally UTF-8-decode the returned bytes into the returned plaintext string.

Process flow

The following diagram visualizes this operation's (unreachable) control flow, mirroring the encrypt path in reverse.

flowchart TD
    Start(["Trigger: none<br>(no caller in this library)"]) --> S1["UTF-8-encode entropy<br>(string overload only)"]
    S1 --> S2["Base64-decode ciphertext<br>(string overload only)"]
    S2 --> D0{Invalid Base64?}
    D0 -->|Yes| ErrFmt(["FormatException<br>(not wrapped)"])
    D0 -->|No| S3["Byte-array overload:<br>InitPrompt, then reset description"]
    S3 --> S4["Initialize ciphertext/entropy BLOBs;<br>call CryptUnprotectData<br>(native, UI forbidden)"]
    S4 --> D1{Call failed?}
    D1 -->|Yes| ErrDec["Exception:<br>unable to decrypt data"]
    D1 -->|No| S5["Copy decrypted bytes<br>into managed byte[]"]
    ErrDec --> Cleanup["finally (byte overload):<br>free unmanaged BLOB memory"]
    S5 --> Cleanup
    Cleanup --> D2{Exception pending?}
    D2 -->|Yes| EndErr(["Exception propagates<br>to caller"])
    D2 -->|No| S6["Byte overload returns byte[];<br>string overloads then<br>UTF-8-decode to plaintext"]
    S6 --> EndOk(["Plaintext/byte[] returned;<br>description populated separately"])

Failure and edge-case behavior

  • This operation has no reachable call path from any member of this library and no InternalsVisibleTo grant, so it is unreachable from any consumer's point of view and has no test coverage*.
  • The internal visibility modifier on Decrypt is not a security boundary: the underlying CryptProtectData/CryptUnprotectData externs are public static extern, so any consumer can call raw DPAPI decryption directly with a captured cookie and the matching entropy, bypassing this unreachable wrapper entirely. See Security and authorization for the full risk discussion.
  • Invalid Base64 input to the string overload throws FormatException directly from Convert.FromBase64String, before the byte-array overload's exception wrapping applies — this failure is not wrapped as Exception("DPAPI was unable to decrypt data.").
  • Any failure raised inside the byte-array overload's try block (including a native CryptUnprotectData failure) is wrapped as a single generic System.Exception("DPAPI was unable to decrypt data."), matching the encrypt path's exception-wrapping behavior (BR-12). This wrapping does not cover the preceding InitPrompt call or the description reset to "" (both run before the try block starts), the string overload's own Encoding.UTF8.GetString call (which runs only after the byte overload has already returned successfully, outside its try/catch), or an exception thrown from the finally cleanup block — failures in those regions propagate as their original, unwrapped exception type.

Note: Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

Sources

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