Skip to content

feat(dpapi): implement DPAPI #381

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
437 changes: 343 additions & 94 deletions Cargo.lock

Large diffs are not rendered by default.

30 changes: 21 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ members = [
"ffi/symbol-rename-macro",
"crates/winscard",
"crates/ffi-types",
"crates/dpapi",
"crates/dpapi-cli-client",
]
exclude = [
"tools/wasm-testcompile",
Expand All @@ -33,20 +35,28 @@ rand = "0.8"
cfg-if = "1"
time = { version = "0.3", default-features = false }
sha1 = { version = "0.10", default-features = false }
sha2 = "0.10"
num-derive = "0.4"
num-traits = { version = "0.2", default-features = false }
picky = { version = "7.0.0-rc.12", default-features = false }
picky-asn1 = "0.10"
picky-asn1-der = "0.5"
picky-asn1-x509 = "0.14"
picky-krb = "0.9"
tokio = "1.43"
ffi-types = { path = "crates/ffi-types" }
winscard = { version = "0.2", path = "crates/winscard" }
dpapi = { version = "0.1.0", path = "crates/dpapi" }
rsa = { version = "0.9.7", default-features = false }
windows-sys = "0.59"
base64 = "0.22"
whoami = "1.5"
tracing-subscriber = "0.3"
proptest = "1.6"
serde = "1"
byteorder = "1.5"
num-bigint-dig = "0.8"
hmac = "0.12"

[features]
default = ["aws-lc-rs"]
Expand Down Expand Up @@ -77,6 +87,7 @@ cfg-if.workspace = true
time = { workspace = true, features = ["std"] }
picky.workspace = true
sha1.workspace = true
sha2.workspace = true
num-derive.workspace = true
num-traits = { workspace = true, default-features = true }
picky-asn1-der.workspace = true
Expand All @@ -87,26 +98,24 @@ tokio = { workspace = true, optional = true, features = ["time", "rt", "rt-multi
winscard = { workspace = true, optional = true }
rsa = { workspace = true, features = ["sha1"] }
tracing = { workspace = true, default-features = true }
serde.workspace = true
picky-krb.workspace = true
picky-asn1 = { workspace = true, features = ["time_conversion"] }
byteorder.workspace = true
num-bigint-dig.workspace = true
hmac.workspace = true
url = "2.5"

byteorder = "1.5"
md-5 = "0.10"
md4 = "0.10"
sha2 = "0.10"
hmac = "0.12"
crypto-mac = "0.11"
lazy_static = "1.5"
serde = "1"
serde_derive = "1"
url = "2.5"
oid = "0.2"

picky-krb = "0.9"
picky-asn1 = { version = "0.10", features = ["time_conversion"] }

reqwest = { version = "0.12", optional = true, default-features = false, features = ["blocking", "rustls-tls-no-provider"] }
hickory-resolver = { version = "0.24", optional = true }
portpicker = { version = "0.1", optional = true }
num-bigint-dig = "0.8"
rustls = { version = "0.23", optional = true, default-features = false, features = ["logging", "std", "tls12"] }
rustls-native-certs = { version = "0.8", optional = true }
zeroize = { version = "1.8", features = ["zeroize_derive"] }
Expand All @@ -117,6 +126,9 @@ winreg = "0.55"
windows = { version = "0.59", features = [ "Win32_Foundation", "Win32_NetworkManagement_Dns"] }
windows-sys = { workspace = true, features = ["Win32_Security_Cryptography", "Win32_Foundation"] }

[target.'cfg(target_arch = "wasm32")'.dependencies]
uuid = { workspace = true, features = ["js"]}

[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
async-dnssd = "0.5"
futures = "0.3"
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The usage of the SSPs is as simple as creating an instance of the security provi

Here is an example of acquiring a credentials handle and a timestamp of their validity:
```rust
use sspi::{CredentialUse, Ntlm, Sspi, Username, builders::EmptyInitializeSecurityContext, OwnedSecurityBuffer, ClientRequestFlags, DataRepresentation, SecurityBufferType, SspiImpl};
use sspi::{CredentialUse, Ntlm, Sspi, Username, builders::EmptyInitializeSecurityContext, SecurityBuffer, ClientRequestFlags, DataRepresentation, BufferType, SspiImpl};

fn main() {
let account_name = "example_user";
Expand All @@ -41,9 +41,9 @@ fn main() {
.execute()
.unwrap();

let mut output_buffer = vec![OwnedSecurityBuffer::new(Vec::new(), SecurityBufferType::Token)];
let mut output_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];
// first time calling initialize_security_context, the input buffer should be empty
let mut input_buffer = vec![OwnedSecurityBuffer::new(Vec::new(), SecurityBufferType::Token)];
let mut input_buffer = vec![SecurityBuffer::new(Vec::new(), BufferType::Token)];

// create a builder for the first call to initialize_security_context
// the target should start with the protocol name, e.g. "HTTP/example.com" or "LDAP/example.com"
Expand Down
9 changes: 9 additions & 0 deletions crates/dpapi-cli-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "dpapi-cli-client"
version = "0.1.0"
edition = "2021"

[dependencies]
xflags = "0.3"
dpapi = { path = "../dpapi" }
tracing-subscriber = { workspace = true, features = ["std", "fmt", "local-time", "env-filter"] }
38 changes: 38 additions & 0 deletions crates/dpapi-cli-client/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::path::PathBuf;

xflags::xflags! {
/// DPAPI cli client. This app is used to encrypt/decrypt secrets using the DPAPI.
cmd dpapi {
/// Target server hostname.
/// For example, win-956cqossjtf.tbt.com.
required --server server: String

/// The username to decrypt/encrypt the DPAPI blob.
/// The username can be specified in FQDN (DOMAIN\username) or UPN (username@domain) format.
required --username username: String

/// User's password.
required --password password: String

/// Client's computer name. This parameter is optional.
/// If not provided, the current computer name will be used.
optional --computer-name computer_name: String

/// Encrypt secret.
/// This command simulates the `NCryptProtectSecret` function. The encrypted secret (DPAPI blob) will be printed to stdout.
cmd encrypt {
/// User's SID.
required --sid sid: String

/// Secret to encrypt.
/// This parameter is optional. If not provided, the app will try to read the secret from stdin.
optional --secret secret: String
}

cmd decrypt {
/// Path to file that contains DPAPI blob.
/// This parameter is optional. If not provided, the app will try to read the DPAPI blob from stdin.
optional --file file: PathBuf
}
}
}
36 changes: 36 additions & 0 deletions crates/dpapi-cli-client/src/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::fs::OpenOptions;

use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;

const DPAPI_LOG_PATH_ENV: &str = "DPAPI_LOG_PATH";

pub fn init_logging() {
let path = if let Ok(path) = std::env::var(DPAPI_LOG_PATH_ENV) {
path
} else {
eprintln!(
"[DPAPI] {} environment variable is not set. Logging is disabled.",
DPAPI_LOG_PATH_ENV
);
return;
};

let file = match OpenOptions::new().create(true).append(true).open(&path) {
Ok(f) => f,
Err(e) => {
eprintln!("[DPAPI] Couldn't open log file: {e}. File path: {}", path);
return;
}
};

let fmt_layer = tracing_subscriber::fmt::layer()
.pretty()
.with_thread_names(true)
.with_writer(file);

tracing_subscriber::registry()
.with(fmt_layer)
.with(EnvFilter::from_env("DPAPI_LOG_LEVEL"))
.init();
}
63 changes: 63 additions & 0 deletions crates/dpapi-cli-client/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
mod cli;
mod logging;

use std::fs;
use std::io::{stdin, stdout, Read, Result, Write};

use crate::cli::{Decrypt, Dpapi, DpapiCmd, Encrypt};

fn run(data: Dpapi) -> Result<()> {
logging::init_logging();

let Dpapi {
server,
username,
password,
computer_name,
subcommand,
} = data;

match subcommand {
DpapiCmd::Encrypt(Encrypt { sid, secret }) => {
let secret = if let Some(secret) = secret {
secret.into_bytes()
} else {
stdin().bytes().collect::<Result<Vec<_>>>()?
};

let blob = dpapi::n_crypt_protect_secret(
secret.into(),
sid,
None,
&server,
&username,
password.into(),
computer_name,
)
.unwrap();

stdout().write_all(&blob)?;
}
DpapiCmd::Decrypt(Decrypt { file }) => {
let blob = if let Some(file) = file {
fs::read(file)?
} else {
stdin().bytes().collect::<Result<Vec<_>>>()?
};

let secret =
dpapi::n_crypt_unprotect_secret(&blob, &server, &username, password.into(), computer_name).unwrap();

stdout().write_all(secret.as_ref())?;
}
}

Ok(())
}

fn main() -> Result<()> {
match Dpapi::from_env() {
Ok(flags) => run(flags),
Err(err) => err.exit(),
}
}
59 changes: 59 additions & 0 deletions crates/dpapi/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
[package]
name = "dpapi"
version = "0.1.0"
edition = "2021"
readme = "README.md"
license = "MIT/Apache-2.0"
homepage = "https://github.com/devolutions/sspi-rs"
repository = "https://github.com/devolutions/sspi-rs"
authors = ["Devolutions Inc. <[email protected]>"]
description = "A Rust implementation of Windows DPAPI"

[lib]
name = "dpapi"

[features]
tsssp = ["sspi/tsssp"]

[dependencies]
bitflags.workspace = true
byteorder.workspace = true
num-derive.workspace = true
num-traits = { workspace = true, default-features = true }
uuid = { workspace = true, features = ["std"] }
picky-asn1.workspace = true
picky-asn1-der.workspace = true
picky-krb.workspace = true
picky-asn1-x509 = { workspace = true, features = ["pkcs7"] }
num-bigint-dig.workspace = true
sha1.workspace = true
sha2.workspace = true
rand.workspace = true
hmac.workspace = true
tracing = { workspace = true, default-features = true }
whoami.workspace = true

kbkdf = "0.0.1"
sha1-pre = { version = "0.11.0-pre.2", package = "sha1" }
sha2-pre = { version = "0.11.0-pre.2", package = "sha2" }
hmac-pre = { version = "0.13.0-pre.4", package = "hmac" }
digest-pre = { version = "0.11.0-pre.9", package = "digest", default-features = false }

elliptic-curve = { version = "0.13", features = ["sec1", "std"] }
p521 = { version = "0.13", features = ["ecdh"] }
p256 = { version = "0.13", features = ["ecdh"] }
p384 = { version = "0.13", features = ["ecdh"] }
concat-kdf = { version = "0.1", features = ["std"] }
typenum = "1.17"
aes-kw = { version = "0.2", features = ["std"] }
aes-gcm = { version = "0.10", features = ["std"] }
url = "2.5"

thiserror = "2.0"
regex = "1.11"

sspi = { path = "../..", features = ["network_client"] }

[dev-dependencies]
paste = "1.0"

9 changes: 9 additions & 0 deletions crates/dpapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# dpapi-rs

This crate contains a Windows [DPAPI](https://learn.microsoft.com/en-us/windows/win32/seccng/cng-dpapi) implementation. It can encrypt the data/decrypt DPAPI blobs using the domain's root key.

It automatically makes RPC calls to obtain the root key. The user must provide credentials to authenticate in the DC.

It implements the [MS-GKDI Group Key Distribution Protocol](https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/943dd4f6-6b80-4a66-8594-80df6d2aad0a).

The original DPAPI supports many [protection descriptors](https://learn.microsoft.com/en-us/windows/win32/seccng/protection-descriptors). This library implements only SID protection descriptor.
Loading