---
title: Decrypt (internal only)
summary: Mirrors the encrypt path to recover plaintext from DPAPI ciphertext; unreachable from any consumer in this library.
status: draft
owner: Framework
last_reviewed: 2026-10-02
library: SAIF.NetCore.LegacyAuth
reference_type: package
operation_id: OP-06
trigger: No caller exists in the library; documented for completeness.
---

# 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](../security-authorization.md).

| 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`.
   - 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.")`.
3. 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.
4. Inside the try block, initialize the ciphertext and entropy `DATA_BLOB`s ([BR-11](../business-rules.md#br-11-no-dpapi-ui-prompts-permitted)), 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.
   - On failure: wrapped as `Exception("DPAPI was unable to decrypt data.")`, mirroring the encrypt path's generic wrapping behavior ([BR-12](../business-rules.md#br-12-generic-exception-wrapping-on-encryption-failure)).
5. On success, copy the decrypted bytes into a managed `byte[]`. A `finally` block frees the unmanaged memory allocated for all three `DATA_BLOB`s 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.

```mermaid
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](../security-authorization.md) 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](../business-rules.md#br-12-generic-exception-wrapping-on-encryption-failure)). 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.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

> **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`
