windows-native-keyring-store 1.1.0

Windows native credential store for keyring
Documentation
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
use byteorder::{ByteOrder, LittleEndian};
use std::collections::HashMap;
use std::iter::once;

use windows_sys::Win32::Foundation::{
    ERROR_BAD_USERNAME, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_SUCH_LOGON_SESSION,
    ERROR_NOT_FOUND, FILETIME, GetLastError,
};
#[cfg(feature = "search")]
use windows_sys::Win32::Security::Credentials::CredEnumerateW;
use windows_sys::Win32::Security::Credentials::{
    CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH,
    CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST, CRED_PERSIST_ENTERPRISE,
    CRED_PERSIST_LOCAL_MACHINE, CRED_PERSIST_SESSION, CRED_TYPE_GENERIC, CREDENTIAL_ATTRIBUTEW,
    CREDENTIALW, CredDeleteW, CredFree, CredReadW, CredWriteW,
};
use zeroize::Zeroize;

#[cfg(feature = "search")]
use crate::cred::Cred;
use keyring_core::error::{Error, Result};

#[derive(Debug, Clone, PartialEq, Eq)]
#[repr(u32)]
pub enum CredPersist {
    Session = CRED_PERSIST_SESSION,
    Local = CRED_PERSIST_LOCAL_MACHINE,
    Enterprise = CRED_PERSIST_ENTERPRISE,
}

impl std::fmt::Display for CredPersist {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            CredPersist::Session => "Session",
            CredPersist::Local => "Local",
            CredPersist::Enterprise => "Enterprise",
        })
    }
}

impl std::str::FromStr for CredPersist {
    type Err = Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        match s.to_ascii_lowercase().as_str() {
            "session" => Ok(CredPersist::Session),
            "local" => Ok(CredPersist::Local),
            "enterprise" => Ok(CredPersist::Enterprise),
            _ => Err(Error::Invalid(
                s.to_string(),
                "must be Session, Local, or Enterprise".to_string(),
            )),
        }
    }
}

pub fn validate_target(target: &str, user: &str) -> Result<()> {
    if user.len() > CRED_MAX_USERNAME_LENGTH as usize {
        return Err(Error::TooLong(
            String::from("user"),
            CRED_MAX_USERNAME_LENGTH,
        ));
    }
    if target.is_empty() {
        return Err(Error::Invalid(
            "target".to_string(),
            "cannot be empty".to_string(),
        ));
    }
    if target.len() > CRED_MAX_GENERIC_TARGET_NAME_LENGTH as usize {
        return Err(Error::TooLong(
            String::from("target"),
            CRED_MAX_GENERIC_TARGET_NAME_LENGTH,
        ));
    }
    Ok(())
}

pub fn validate_password(password: &str) -> Result<Vec<u8>> {
    let mut blob_u16 = to_wstr_no_null(password);
    let mut blob = vec![0; blob_u16.len() * 2];
    LittleEndian::write_u16_into(&blob_u16, &mut blob);
    blob_u16.zeroize();
    if blob.len() > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize {
        blob.zeroize();
        Err(Error::TooLong(
            String::from("password encoded as UTF-16"),
            CRED_MAX_CREDENTIAL_BLOB_SIZE,
        ))
    } else {
        // caller will zeroize the blob
        Ok(blob)
    }
}

pub fn validate_secret(secret: &[u8]) -> Result<()> {
    if secret.len() > CRED_MAX_CREDENTIAL_BLOB_SIZE as usize {
        return Err(Error::TooLong(
            String::from("secret"),
            CRED_MAX_CREDENTIAL_BLOB_SIZE,
        ));
    }
    Ok(())
}

pub fn validate_attributes(username: &str, target_alias: &str, comment: &str) -> Result<()> {
    if username.len() > CRED_MAX_USERNAME_LENGTH as usize {
        return Err(Error::TooLong(
            String::from("user"),
            CRED_MAX_USERNAME_LENGTH,
        ));
    }
    if target_alias.len() > CRED_MAX_STRING_LENGTH as usize {
        return Err(Error::TooLong(
            String::from("target_alias"),
            CRED_MAX_STRING_LENGTH,
        ));
    }
    if comment.len() > CRED_MAX_STRING_LENGTH as usize {
        return Err(Error::TooLong(
            String::from("comment"),
            CRED_MAX_STRING_LENGTH,
        ));
    }
    Ok(())
}

/// Save or create a generic credential with pre-validated data
pub fn save_credential(
    target_name: &str,
    user: &str,
    target_alias: &str,
    comment: &str,
    secret: &[u8],
    persistence: &CredPersist,
) -> Result<()> {
    let mut username = to_wstr(user);
    let mut target_name = to_wstr(target_name);
    let mut target_alias = to_wstr(target_alias);
    let mut comment = to_wstr(comment);
    let mut blob = secret.to_vec();
    let blob_len = blob.len() as u32;
    let flags = CRED_FLAGS::default();
    let cred_type = CRED_TYPE_GENERIC;
    let persist = persistence.clone() as CRED_PERSIST;
    // Ignored by CredWriteW
    let last_written = FILETIME {
        dwLowDateTime: 0,
        dwHighDateTime: 0,
    };
    let attribute_count = 0;
    let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut();
    let credential = CREDENTIALW {
        Flags: flags,
        Type: cred_type,
        TargetName: target_name.as_mut_ptr(),
        Comment: comment.as_mut_ptr(),
        LastWritten: last_written,
        CredentialBlobSize: blob_len,
        CredentialBlob: blob.as_mut_ptr(),
        Persist: persist,
        AttributeCount: attribute_count,
        Attributes: attributes,
        TargetAlias: target_alias.as_mut_ptr(),
        UserName: username.as_mut_ptr(),
    };
    // Call windows API
    let result = match unsafe { CredWriteW(&credential, 0) } {
        0 => Err(decode_error()),
        _ => Ok(()),
    };
    // erase the copy of the secret
    blob.zeroize();
    result
}

/// Delete a generic credential
pub fn delete_credential(target_name: &str) -> Result<()> {
    let target_name = to_wstr(target_name);
    let cred_type = CRED_TYPE_GENERIC;
    match unsafe { CredDeleteW(target_name.as_ptr(), cred_type, 0) } {
        0 => Err(decode_error()),
        _ => Ok(()),
    }
}

/// Enumerate generic credentials
#[cfg(feature = "search")]
pub fn enumerate_credentials(
    pattern: Option<regex::Regex>,
    delimiters: &[String; 3],
) -> Result<Vec<Cred>> {
    let spec = format!(
        "^{}(.*){}(.*){}$",
        regex::escape(&delimiters[0]),
        regex::escape(&delimiters[1]),
        regex::escape(&delimiters[2])
    );
    let spec_pat = regex::Regex::new(&spec).unwrap();
    let mut count: u32 = 0;
    let mut creds = std::ptr::null_mut();
    if unsafe { CredEnumerateW(std::ptr::null(), 0, &mut count, &mut creds) } == 0 {
        return match decode_error() {
            Error::NoEntry => Ok(Vec::new()),
            err => Err(err),
        };
    }
    let slice = unsafe { std::slice::from_raw_parts(creds, count as usize) };
    let mut result = Vec::new();
    for cred in slice {
        let mut candidate = cred_from_credential(&mut unsafe { **cred });
        if let Some(pat) = &pattern
            && !pat.is_match(&candidate.target_name)
        {
            continue;
        }
        if let Some(captures) = spec_pat.captures(&candidate.target_name) {
            // user comes first, service second in the target name. Specifiers are the other way.
            candidate.specifiers = Some((captures[2].to_string(), captures[1].to_string()))
        }
        result.push(candidate)
    }
    unsafe { CredFree(creds as *mut std::ffi::c_void) };
    Ok(result)
}

/// Run a function over a generic credential to extract data from it.
pub fn extract_from_credential<F, T>(target_name: &str, f: F) -> Result<T>
where
    F: FnOnce(&CREDENTIALW) -> Result<T>,
{
    let mut p_credential = std::ptr::null_mut();
    // at this point, p_credential is just a pointer to nowhere.
    // The allocation happens in the `CredReadW` call below.
    let result = {
        let cred_type = CRED_TYPE_GENERIC;
        let target_name = to_wstr(target_name);
        unsafe { CredReadW(target_name.as_ptr(), cred_type, 0, &mut p_credential) }
    };
    match result {
        0 => {
            // `CredReadW` failed, so no allocation has been done, so no free needs to be done
            Err(decode_error())
        }
        _ => {
            // `CredReadW` succeeded, so p_credential points at an allocated credential. Apply
            // the passed extractor function to it.
            let result = f(unsafe { &*p_credential });
            // Finally, we erase the secret and free the allocated credential.
            erase_secret(unsafe { &mut *p_credential });
            unsafe { CredFree(p_credential as *mut _) };
            result
        }
    }
}

/// get a Cred from a native credential
#[cfg(feature = "search")]
pub fn cred_from_credential(credential: &mut CREDENTIALW) -> Cred {
    erase_secret(credential); // erase the secret, so it won't be leaked into the heap
    let persistence = match credential.Persist {
        CRED_PERSIST_SESSION => CredPersist::Session,
        CRED_PERSIST_LOCAL_MACHINE => CredPersist::Local,
        _ => CredPersist::Enterprise,
    };
    let target_name = unsafe { from_wstr(credential.TargetName) };
    Cred {
        target_name,
        specifiers: None,
        persistence,
    }
}

/// A password extractor for use with [extract_from_credential].
pub fn extract_password(credential: &CREDENTIALW) -> Result<String> {
    let mut blob = extract_secret(credential)?;
    // 3rd parties may write credential data with an odd number of bytes,
    // so we make sure that we don't try to decode those as utf16
    if blob.len() % 2 != 0 {
        return Err(Error::BadEncoding(blob));
    }
    // This should be a UTF-16 string, so convert it to
    // a UTF-16 vector and then try to decode it.
    let mut blob_u16 = vec![0; blob.len() / 2];
    LittleEndian::read_u16_into(&blob, &mut blob_u16);
    let result = match String::from_utf16(&blob_u16) {
        Err(_) => Err(Error::BadEncoding(blob)),
        Ok(s) => {
            // we aren't returning the blob, so clear it
            blob.zeroize();
            Ok(s)
        }
    };
    // we aren't returning the utf16 blob, so clear it
    blob_u16.zeroize();
    result
}

/// A secret extractor for use with [extract_from_credential].
pub fn extract_secret(credential: &CREDENTIALW) -> Result<Vec<u8>> {
    let blob_pointer: *const u8 = credential.CredentialBlob;
    let blob_len: usize = credential.CredentialBlobSize as usize;
    if blob_len == 0 {
        return Ok(Vec::new());
    }
    let blob = unsafe { std::slice::from_raw_parts(blob_pointer, blob_len) };
    Ok(blob.to_vec())
}

/// A metadata extractor for use with [extract_from_credential].
pub fn extract_attributes(credential: &CREDENTIALW) -> Result<HashMap<String, String>> {
    let result = HashMap::from([
        ("target_name".to_string(), unsafe {
            from_wstr(credential.TargetName)
        }),
        ("username".to_string(), unsafe {
            from_wstr(credential.UserName)
        }),
        ("target_alias".to_string(), unsafe {
            from_wstr(credential.TargetAlias)
        }),
        ("comment".to_string(), unsafe {
            from_wstr(credential.Comment)
        }),
        (
            "persistence".to_string(),
            match credential.Persist {
                CRED_PERSIST_SESSION => CredPersist::Session.to_string(),
                CRED_PERSIST_LOCAL_MACHINE => CredPersist::Local.to_string(),
                _ => CredPersist::Enterprise.to_string(),
            },
        ),
    ]);
    Ok(result)
}

/// helper for extract_from_platform
fn erase_secret(credential: &mut CREDENTIALW) {
    let blob_pointer: *mut u8 = credential.CredentialBlob;
    let blob_len: usize = credential.CredentialBlobSize as usize;
    if blob_len == 0 {
        return;
    }
    let blob = unsafe { std::slice::from_raw_parts_mut(blob_pointer, blob_len) };
    blob.zeroize();
}

fn to_wstr(s: &str) -> Vec<u16> {
    s.encode_utf16().chain(once(0)).collect()
}

fn to_wstr_no_null(s: &str) -> Vec<u16> {
    s.encode_utf16().collect()
}

unsafe fn from_wstr(ws: *const u16) -> String {
    // null pointer case, return empty string
    if ws.is_null() {
        return String::new();
    }
    // this code from https://stackoverflow.com/a/48587463/558006
    let len = (0..).take_while(|&i| unsafe { *ws.offset(i) != 0 }).count();
    if len == 0 {
        return String::new();
    }
    let slice = unsafe { std::slice::from_raw_parts(ws, len) };
    String::from_utf16_lossy(slice)
}

/// Windows error codes are `DWORDS` which are 32-bit unsigned ints.
#[derive(Debug)]
pub struct PlatformError(pub u32);

impl std::fmt::Display for PlatformError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self.0 {
            ERROR_NO_SUCH_LOGON_SESSION => write!(f, "Windows ERROR_NO_SUCH_LOGON_SESSION"),
            ERROR_NOT_FOUND => write!(f, "Windows ERROR_NOT_FOUND"),
            ERROR_BAD_USERNAME => write!(f, "Windows ERROR_BAD_USERNAME"),
            ERROR_INVALID_FLAGS => write!(f, "Windows ERROR_INVALID_FLAGS"),
            ERROR_INVALID_PARAMETER => write!(f, "Windows ERROR_INVALID_PARAMETER"),
            err => write!(f, "Windows error code {err}"),
        }
    }
}

impl std::error::Error for PlatformError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None
    }
}

/// Map the last encountered Windows API error to a crate error with appropriate annotation.
pub fn decode_error() -> Error {
    match unsafe { GetLastError() } {
        ERROR_NOT_FOUND => Error::NoEntry,
        ERROR_NO_SUCH_LOGON_SESSION => Error::NoStorageAccess(wrap(ERROR_NO_SUCH_LOGON_SESSION)),
        err => Error::PlatformFailure(wrap(err)),
    }
}

fn wrap(code: u32) -> Box<dyn std::error::Error + Send + Sync> {
    Box::new(PlatformError(code))
}