1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#![deny(unsafe_op_in_unsafe_fn)]
use chrono::{DateTime, Utc};
use std::{mem, ptr, usize};
use windows_sys::Win32::{
Foundation::GetLastError,
Security::Cryptography::{NCryptUnprotectSecret, NCRYPT_SILENT_FLAG},
};
use windows_sys::{
core::HRESULT,
Win32::Foundation::{LocalFree, HLOCAL},
};
use crate::{
helpers::{convert_to_uint32, filetime_to_datetime},
LapsError, MsLapsPassword,
};
#[derive(Debug, PartialEq)]
struct EncryptedPasswordAttributePrefixInfo {
_timestamp: DateTime<Utc>,
encrypted_buffer_size: usize,
_flags_reserved: u32,
}
impl TryFrom<&[u8]> for EncryptedPasswordAttributePrefixInfo {
type Error = LapsError;
/// This will take the first 16 bytes of the password attribute and convert its parts to the corresponding rust types.
///
/// ```plain
/// 0 1 2 3
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | dwHighDateTime (most significant part of filetime struct) |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | dwLowDateTime (least significant part of filetime struct) |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | size of the encrypted password buffer |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | reserved flags for future use |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// ```
/// All numbers are 32 bit numbers in LSB byte order
///
/// # Returns None on inputs with `buf.len() < 16`
///
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
if value.len() < 16 {
return Err(LapsError::InvalidBufLen);
}
let parts: Vec<u32> = value
.chunks(4)
.take(4)
.flat_map(convert_to_uint32)
.collect();
let time_offset: i64 = ((parts[0] as i64) << 32) | parts[1] as i64;
Ok(Self {
_timestamp: filetime_to_datetime(time_offset),
encrypted_buffer_size: parts[2] as usize,
_flags_reserved: parts[3],
})
}
}
pub(crate) struct EncryptedPasswordAttribute {
_prefix: EncryptedPasswordAttributePrefixInfo,
data: Vec<u8>,
}
impl TryFrom<&[u8]> for EncryptedPasswordAttribute {
type Error = LapsError;
/// will convert the encrypted password attribute
///
/// This will return None in case of a Invalid Buffer Length
///
/// The first 16 bytes of the attribute are the PrefixInfo and will be parsed.
/// The rest is the data. The data will be checked for size.
/// The data will __not__ be decrypted at this staged yet.
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
let prefix: EncryptedPasswordAttributePrefixInfo = value.try_into()?;
let encrypted_buffer_size = prefix.encrypted_buffer_size;
if value.len() != encrypted_buffer_size + 16 {
// Whole blob is too short
return Err(LapsError::BlobTooShort);
}
Ok(Self {
_prefix: prefix,
data: value[16..(16 + encrypted_buffer_size)].to_owned(),
})
}
}
pub(crate) trait DecryptLapsPassword {
fn decrypt(attr: &EncryptedPasswordAttribute) -> Result<MsLapsPassword, LapsError>;
}
impl DecryptLapsPassword for EncryptedPasswordAttribute {
fn decrypt(pass: &EncryptedPasswordAttribute) -> Result<MsLapsPassword, LapsError> {
let parsed = decrypt_password_blob_ng(pass)?;
serde_json::from_str(&parsed).map_err(|_| {
LapsError::ConversionError(
"The decrypted msLAPS-EncryptedPassword is not a valid JSON String".into(),
)
})
}
}
/// Wrapper to a raw pointer as to handle gracefully freeing it
struct DroppablePointer(*mut *mut u8);
impl DroppablePointer {
fn new() -> Self {
Self(&mut ptr::null_mut())
}
}
impl Default for DroppablePointer {
fn default() -> Self {
Self::new()
}
}
impl Drop for DroppablePointer {
fn drop(&mut self) {
// safety:
// This should always work. In case there is an error with `LocalFree` we simply log it out, since we can't do anything about that.
// Double free should also not happen since this is the drop() call and that will be checked by rustc
// In case the pointer was not allocated at all and still is a NULL pointer this will also not fail.
// see also https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-localfree#remarks
if unsafe { LocalFree(self.0 as HLOCAL) }.is_null() {
return;
}
if cfg!(debug_assertions) {
let err = unsafe { GetLastError() };
panic!("bug: undefined behavior: freeing {:?} failed. Err Code: {err} (typically this means the pointer didn't belong to the allocator, or there was heap corruption)", self.0);
}
}
}
/// uses DPAPI NG to decrypt an encrypted LAPS password BLOB
///
/// This function uses the credentials of the current process/user.
///
/// This function calls a bunch of `unsafe` internal windows functions.
///
/// This function should be safe to call. Every return is checked for errors.
fn decrypt_password_blob_ng(pass: &EncryptedPasswordAttribute) -> Result<String, LapsError> {
// get the pointer to the data blob to hand to NCryptUnprotectSecret
// this must be mut since NCryptUnprotectSecret expect a *mut
let buf_ptr = pass.data.as_ptr();
let buf_len = pass.data.len() as u32;
// this pointer will be set by NCryptUnprotectSecret and will then point to the array of the encrypted bytes
// DroppablePointer is used to call LocalFree() on drop
let buf_out_ptr: DroppablePointer = DroppablePointer::default();
// this will be set by NCryptUnprotectSecret and will cointain the size of the encrypted buffer
let mut buf_out_len = 0_u32;
// call to NCryptUnprotectSecret
// https://learn.microsoft.com/en-us/windows/win32/api/ncryptprotect/nf-ncryptprotect-ncryptunprotectsecret
// SECURITY_STATUS NCryptUnprotectSecret(
// [out, optional] NCRYPT_DESCRIPTOR_HANDLE *phDescriptor, Pointer to the protection descriptor handle.
// [in] DWORD dwFlags,
// [in] const BYTE *pbProtectedBlob,
// ULONG cbProtectedBlob,
// [in, optional] const NCRYPT_ALLOC_PARA *pMemPara,
// [in, optional] HWND hWnd,
// [out] BYTE **ppbData,
// [out] ULONG *pcbData
// );
// safety: Every input is know to us and the outputs will be handled further down.
// buf_out_ptr is a Droppable pointer which will satisfy the need to be LocalFree'd when dropping it.
let uprotect_result: HRESULT = unsafe {
NCryptUnprotectSecret(
ptr::null_mut(), // this is not needed for our usecase
NCRYPT_SILENT_FLAG, // Requests that the key service provider not display a user interface.
buf_ptr, // Pointer to an array of bytes that contains the data to decrypt.
buf_len, // The number of bytes in the array pointed to by the pbProtectedBlob parameter.
ptr::null(), // since this is set to null we need to free the memory ourselves by calling LocalFree. This will be handled by DroppablePointer::drop()
0, // Handle to the parent window of the user interface, if any, to be displayed.
buf_out_ptr.0, // Address of a variable that receives a pointer to the decrypted data.
&mut buf_out_len, // Pointer to a ULONG variable that contains the size, in bytes, of the decrypted data pointed to by the ppbData variable.
)
} as _;
if uprotect_result != 0 {
// there was an error decrypting the result
return Err(LapsError::DpapiFailedToDecrypt(uprotect_result));
}
if buf_out_ptr.0.is_null() || buf_out_len == 0 {
// something went wrong within the memory allocation & decryption
// this should be checked by uprotect_result but we will check it anyway since we want to use those things later
return Err(LapsError::Other(
"Decrypted buffer is invalid or of size 0".into(),
));
}
// convert buf_out_len to usize. This should not fail on modern windows computers
let buf_out_len: usize = buf_out_len
.try_into()
.map_err(|_| LapsError::InvalidBufLen)?;
// Check that len * mem::size_of::<T>() fits into an isize since that is needed to safely call std::slice::from_raw_parts()
let _: isize = (buf_out_len * mem::size_of::<u8>())
.try_into()
.map_err(|_| LapsError::InvalidBufLen)?;
// at this point we know both the length of the buffer as well as the location of the buffer
let res: Vec<u8> =
// safety: since both the length as well as the location is known and they are not null this is safe
// NCryptUnprotectSecret uses LocalAlloc (https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-localalloc) in the back
// LocalAlloc will allocate buf_out_len number of bytes. These should be continuous.
unsafe { std::slice::from_raw_parts(*buf_out_ptr.0, buf_out_len) }.to_owned();
if res.len() != buf_out_len {
// there was some error within the slice copy process.
return Err(LapsError::InvalidBufLen);
}
// at this point we should have copied everything we needed from the buffer and can free the memory allocated by NCryptUnprotectSecret
drop(buf_out_ptr);
// Conversion to UTF16 from UTF8
let mut res: Vec<u16> = res
.chunks(2)
.map(|a| (a[1] as u16) << 2 | a[0] as u16)
.collect();
// The String is NULL-terminated. So we remove the last NULL byte
assert!(res.last() == Some(&0));
let _ = res.pop();
String::from_utf16(&res)
.map_err(|_| LapsError::ConversionError("Conversion from UTF16 failed".into()))
}
#[cfg(test)]
mod prefix_tests {
use crate::LapsError;
use super::EncryptedPasswordAttributePrefixInfo;
use chrono::{DateTime, Utc};
#[test]
fn new() {
// this is the Header we will construct in parts further down
let test = EncryptedPasswordAttributePrefixInfo {
_timestamp: DateTime::<Utc>::from_timestamp_nanos(-1262304000000000000),
encrypted_buffer_size: 0x87654321,
_flags_reserved: 0xDEADBEEF,
};
// header _must_ be at least 16 bytes long
let mut header: Vec<u8> = Vec::with_capacity(16);
// this is 103821696000000000 in HEX bytes in LSB Byte Order
// the conversion for 103821696000000000 is tested in the conversion test of filetime_to_timestamp
// upper (Most significant) bytes of the timestamp in LSB
let timestamp_upper: [u8; 4] = [0x48, 0xd9, 0x70, 0x01];
// lower (least significant) bytes of the timestamp in LSB
let timestamp_lower: [u8; 4] = [0x00, 0x00, 0x0f, 0x4e];
header.extend_from_slice(×tamp_upper);
header.extend_from_slice(×tamp_lower);
let buf_size: [u8; 4] = [0x21, 0x43, 0x65, 0x87];
header.extend_from_slice(&buf_size);
let reserved: [u8; 4] = [0xEF, 0xBE, 0xAD, 0xDE];
header.extend_from_slice(&reserved);
let res: Result<EncryptedPasswordAttributePrefixInfo, LapsError> =
header.as_slice().try_into();
assert!(res.is_ok());
assert_eq!(res.expect("res is ok"), test)
}
}