Skip to main content

kc/
records.rs

1//! Typed records: the attribute sets stored for an item and for its key.
2//!
3//! Attribute values reach the file as a positional list ordered by the relation's
4//! schema, which is easy to get subtly wrong and impossible to read. These
5//! structs name every field, carry the values macOS writes, and lower themselves
6//! into the relation's order at the end.
7//!
8//! The values are not invented. `securityd` queries a key record for attributes
9//! such as `KeyCreator` and `Alias`, and returns `errSecNoSuchAttr` when one is
10//! absent — so a key record has to carry the whole set, empty values included.
11//! `tests/keychain_records.rs` checks each field against a key record written by
12//! macOS.
13
14use crate::cssm::Guid;
15use crate::format::Value;
16use crate::schema::Relation;
17
18/// Assigns values to a relation's attributes by name.
19struct AttributeWriter<'r> {
20    relation: &'r Relation,
21    values: Vec<Option<Value>>,
22}
23
24impl<'r> AttributeWriter<'r> {
25    fn new(relation: &'r Relation) -> Self {
26        Self {
27            relation,
28            values: vec![None; relation.attributes.len()],
29        }
30    }
31
32    /// Set an attribute. Names the relation does not have are ignored: schemas
33    /// differ between macOS versions, and a missing optional attribute is not a
34    /// reason to fail a write.
35    fn set(&mut self, name: &str, value: Value) -> &mut Self {
36        if let Some(index) = self.relation.index_of(name) {
37            self.values[index] = Some(value);
38        }
39        self
40    }
41
42    fn flag(&mut self, name: &str, value: bool) -> &mut Self {
43        self.set(name, Value::Uint32(u32::from(value)))
44    }
45
46    fn finish(self) -> Vec<Option<Value>> {
47        self.values
48    }
49}
50
51/// The attribute set of the symmetric key that protects one item.
52///
53/// Every field here holds the value macOS writes for an item key. Two are worth
54/// pointing out because they look wrong:
55///
56/// * `key_class` is `17`, the symmetric-key *relation* id, not a
57///   `CSSM_KEYCLASS` constant.
58/// * `print_name` is the 20-byte `ssgp` label rather than a readable name.
59#[derive(Debug, Clone)]
60pub struct ItemKeyRecord {
61    pub key_class: u32,
62    pub print_name: Vec<u8>,
63    pub alias: Vec<u8>,
64    pub permanent: bool,
65    pub private: bool,
66    pub modifiable: bool,
67    /// The label shared with the item's `ssgp` payload; how the two are linked.
68    pub label: [u8; 20],
69    pub application_tag: Vec<u8>,
70    /// The CSP that created the key, as a braced GUID string with a terminator.
71    pub key_creator: Vec<u8>,
72    /// `CSSM_ALGID_3DES_3KEY_EDE`.
73    pub key_type: u32,
74    pub key_size_in_bits: u32,
75    pub effective_key_size: u32,
76    /// Eight zero bytes for "unset", stored as a blob rather than a date.
77    pub start_date: Vec<u8>,
78    pub end_date: Vec<u8>,
79    pub sensitive: bool,
80    pub always_sensitive: bool,
81    pub extractable: bool,
82    pub never_extractable: bool,
83    pub encrypt: bool,
84    pub decrypt: bool,
85    pub derive: bool,
86    pub sign: bool,
87    pub verify: bool,
88    pub sign_recover: bool,
89    pub verify_recover: bool,
90    pub wrap: bool,
91    pub unwrap: bool,
92}
93
94/// `KeyClass` as stored in a symmetric-key record: the relation id.
95pub const KEY_CLASS_SYMMETRIC: u32 = 0x11;
96
97/// `CSSM_ALGID_3DES_3KEY_EDE`, as stored in `KeyType`.
98pub const KEY_TYPE_3DES_3KEY: u32 = 0x11;
99
100impl ItemKeyRecord {
101    /// The record macOS writes for the key of a password item.
102    pub fn for_item_key(label: [u8; 20]) -> Self {
103        Self {
104            key_class: KEY_CLASS_SYMMETRIC,
105            print_name: label.to_vec(),
106            alias: Vec::new(),
107            permanent: true,
108            private: false,
109            modifiable: false,
110            label,
111            application_tag: Vec::new(),
112            key_creator: format!("{{{}}}\0", Self::creator_guid()).into_bytes(),
113            key_type: KEY_TYPE_3DES_3KEY,
114            key_size_in_bits: 192,
115            effective_key_size: 192,
116            start_date: vec![0u8; 8],
117            end_date: vec![0u8; 8],
118            // The key may encrypt and decrypt its item, and nothing else. It is
119            // sensitive and never leaves the keychain in the clear.
120            sensitive: true,
121            always_sensitive: true,
122            extractable: false,
123            never_extractable: true,
124            encrypt: true,
125            decrypt: true,
126            derive: false,
127            sign: false,
128            verify: false,
129            sign_recover: false,
130            verify_recover: false,
131            wrap: false,
132            unwrap: false,
133        }
134    }
135
136    /// The Apple CSP GUID, formatted the way `KeyCreator` spells it.
137    fn creator_guid() -> String {
138        let guid = Guid::APPLE_CSP;
139        format!(
140            "{:08x}-{:04x}-{:04x}-{:02x}{:02x}-{}",
141            guid.data1,
142            guid.data2,
143            guid.data3,
144            guid.data4[0],
145            guid.data4[1],
146            guid.data4[2..]
147                .iter()
148                .map(|byte| format!("{byte:02x}"))
149                .collect::<String>()
150        )
151    }
152
153    /// Lower into the relation's attribute order.
154    pub fn to_attributes(&self, relation: &Relation) -> Vec<Option<Value>> {
155        let mut writer = AttributeWriter::new(relation);
156        writer
157            .set("KeyClass", Value::Uint32(self.key_class))
158            .set("PrintName", Value::Blob(self.print_name.clone()))
159            .set("Alias", Value::Blob(self.alias.clone()))
160            .flag("Permanent", self.permanent)
161            .flag("Private", self.private)
162            .flag("Modifiable", self.modifiable)
163            .set("Label", Value::Blob(self.label.to_vec()))
164            .set("ApplicationTag", Value::Blob(self.application_tag.clone()))
165            .set("KeyCreator", Value::Blob(self.key_creator.clone()))
166            .set("KeyType", Value::Uint32(self.key_type))
167            .set("KeySizeInBits", Value::Uint32(self.key_size_in_bits))
168            .set("EffectiveKeySize", Value::Uint32(self.effective_key_size))
169            .set("StartDate", Value::Blob(self.start_date.clone()))
170            .set("EndDate", Value::Blob(self.end_date.clone()))
171            .flag("Sensitive", self.sensitive)
172            .flag("AlwaysSensitive", self.always_sensitive)
173            .flag("Extractable", self.extractable)
174            .flag("NeverExtractable", self.never_extractable)
175            .flag("Encrypt", self.encrypt)
176            .flag("Decrypt", self.decrypt)
177            .flag("Derive", self.derive)
178            .flag("Sign", self.sign)
179            .flag("Verify", self.verify)
180            .flag("SignRecover", self.sign_recover)
181            .flag("VerifyRecover", self.verify_recover)
182            .flag("Wrap", self.wrap)
183            .flag("Unwrap", self.unwrap);
184        writer.finish()
185    }
186}
187
188/// The attribute set of a password item.
189#[derive(Debug, Clone, Default)]
190pub struct PasswordRecord {
191    /// `cdat` and `mdat`, as `YYYYMMDDhhmmssZ`.
192    pub created: String,
193    pub modified: String,
194    /// `PrintName`: what Keychain Access shows.
195    pub print_name: String,
196    /// `desc`, the "kind".
197    pub description: Option<String>,
198    /// `icmt`, a free-text comment.
199    pub comment: Option<String>,
200    /// `acct`
201    pub account: Option<String>,
202    /// `svce`, generic items.
203    pub service: Option<String>,
204    /// `gena`, generic items.
205    pub generic: Option<Vec<u8>>,
206    /// `srvr`, internet and AppleShare items.
207    pub server: Option<String>,
208    /// `sdmn`, internet items.
209    pub security_domain: Option<String>,
210    /// `path`, internet items.
211    pub path: Option<String>,
212    /// `port`, internet items.
213    pub port: Option<u32>,
214    /// `ptcl`, internet and AppleShare items: a four-char code held as an integer.
215    pub protocol: Option<[u8; 4]>,
216    /// `atyp`, internet items: a four-char code held as a blob.
217    pub auth_type: Option<[u8; 4]>,
218    /// `vlme`, AppleShare items.
219    pub volume: Option<String>,
220    /// `addr`, AppleShare items.
221    pub address: Option<String>,
222    /// `ssig`, AppleShare items.
223    pub signature: Option<String>,
224}
225
226/// Attributes macOS always stores on a password item, empty when unset. Which of
227/// them a record actually has depends on its relation: `gena` exists only on
228/// generic items, `sdmn` only on internet and AppleShare items.
229///
230/// The distinction between "absent" and "present but empty" is not cosmetic: an
231/// item that omits one of these is invisible to `security find-internet-password`
232/// and to `security dump-keychain`.
233const ALWAYS_PRESENT: [&str; 4] = ["desc", "icmt", "sdmn", "gena"];
234
235impl PasswordRecord {
236    pub fn to_attributes(&self, relation: &Relation) -> Vec<Option<Value>> {
237        let mut writer = AttributeWriter::new(relation);
238        writer
239            .set("cdat", Value::Date(date_field(&self.created)))
240            .set("mdat", Value::Date(date_field(&self.modified)))
241            .set(
242                "PrintName",
243                Value::Blob(self.print_name.as_bytes().to_vec()),
244            );
245
246        let mut text = |name: &str, value: &Option<String>| match value {
247            Some(value) => {
248                writer.set(name, Value::Blob(value.as_bytes().to_vec()));
249            }
250            None if ALWAYS_PRESENT.contains(&name) => {
251                writer.set(name, Value::Blob(Vec::new()));
252            }
253            None => {}
254        };
255        text("desc", &self.description);
256        text("icmt", &self.comment);
257        text("acct", &self.account);
258        text("svce", &self.service);
259        text("srvr", &self.server);
260        text("sdmn", &self.security_domain);
261        text("path", &self.path);
262        text("vlme", &self.volume);
263        text("addr", &self.address);
264        text("ssig", &self.signature);
265
266        writer.set(
267            "gena",
268            Value::Blob(self.generic.clone().unwrap_or_default()),
269        );
270        if let Some(port) = self.port {
271            writer.set("port", Value::Uint32(port));
272        }
273        if let Some(protocol) = self.protocol {
274            writer.set("ptcl", Value::Uint32(u32::from_be_bytes(protocol)));
275        }
276        if let Some(auth_type) = self.auth_type {
277            writer.set("atyp", Value::Blob(auth_type.to_vec()));
278        }
279        writer.finish()
280    }
281}
282
283/// `CertType` for an X.509 v3 certificate (`CSSM_CERT_X_509v3`).
284pub const CERT_TYPE_X509_V3: u32 = 1;
285
286/// `CertEncoding` for DER (`CSSM_CERT_ENCODING_DER`).
287pub const CERT_ENCODING_DER: u32 = 3;
288
289/// `KeyClass` as stored in a private-key record: the relation id, matching the
290/// way a symmetric-key record stores `17`.
291pub const KEY_CLASS_PRIVATE: u32 = 0x10;
292
293/// `CSSM_ALGID_RSA`, as stored in a private key's `KeyType`.
294pub const KEY_TYPE_RSA: u32 = 42;
295
296/// The attribute set of a certificate record.
297///
298/// Every field except the two format tags is a copy of bytes from the
299/// certificate itself; see [`crate::der`]. The record's key data is the
300/// certificate DER, stored in the clear — a certificate is public.
301#[derive(Debug, Clone)]
302pub struct CertificateRecord {
303    pub cert_type: u32,
304    pub cert_encoding: u32,
305    /// `PrintName` and `Alias`, which macOS sets to the same label.
306    pub print_name: String,
307    pub alias: Vec<u8>,
308    pub subject: Vec<u8>,
309    pub issuer: Vec<u8>,
310    pub serial_number: Vec<u8>,
311    /// Absent when the certificate carries no such extension.
312    pub subject_key_identifier: Option<Vec<u8>>,
313    /// SHA-1 of the public key bits: the value that links this certificate to
314    /// its private key, whose `Label` holds the same 20 bytes.
315    pub public_key_hash: [u8; 20],
316}
317
318impl CertificateRecord {
319    /// The record macOS writes for an imported certificate.
320    pub fn for_certificate(label: &str, certificate: &crate::der::Certificate<'_>) -> Self {
321        Self {
322            cert_type: CERT_TYPE_X509_V3,
323            cert_encoding: CERT_ENCODING_DER,
324            print_name: label.to_string(),
325            alias: label.as_bytes().to_vec(),
326            subject: certificate.subject.to_vec(),
327            issuer: certificate.issuer.to_vec(),
328            serial_number: certificate.serial_number.to_vec(),
329            subject_key_identifier: certificate.subject_key_identifier.map(<[u8]>::to_vec),
330            public_key_hash: certificate.public_key_hash(),
331        }
332    }
333
334    pub fn to_attributes(&self, relation: &Relation) -> Vec<Option<Value>> {
335        let mut writer = AttributeWriter::new(relation);
336        writer
337            .set("CertType", Value::Uint32(self.cert_type))
338            .set("CertEncoding", Value::Uint32(self.cert_encoding))
339            .set(
340                "PrintName",
341                Value::Blob(self.print_name.as_bytes().to_vec()),
342            )
343            .set("Alias", Value::Blob(self.alias.clone()))
344            .set("Subject", Value::Blob(self.subject.clone()))
345            .set("Issuer", Value::Blob(self.issuer.clone()))
346            .set("SerialNumber", Value::Blob(self.serial_number.clone()))
347            .set("PublicKeyHash", Value::Blob(self.public_key_hash.to_vec()));
348        if let Some(identifier) = &self.subject_key_identifier {
349            writer.set("SubjectKeyIdentifier", Value::Blob(identifier.clone()));
350        }
351        writer.finish()
352    }
353}
354
355/// The attribute set of a private-key record.
356///
357/// The usage flags are all set, which is what macOS writes for a key imported
358/// from a PKCS#12 bundle: the key may sign, decrypt, unwrap and derive.
359#[derive(Debug, Clone)]
360pub struct PrivateKeyRecord {
361    pub key_class: u32,
362    pub print_name: String,
363    pub alias: Vec<u8>,
364    pub permanent: bool,
365    pub private: bool,
366    pub modifiable: bool,
367    /// The public key hash, shared with the certificate's `PublicKeyHash`.
368    pub label: [u8; 20],
369    pub application_tag: Vec<u8>,
370    pub key_creator: Vec<u8>,
371    pub key_type: u32,
372    pub key_size_in_bits: u32,
373    pub effective_key_size: u32,
374    pub start_date: Vec<u8>,
375    pub end_date: Vec<u8>,
376    pub sensitive: bool,
377    pub always_sensitive: bool,
378    pub extractable: bool,
379    pub never_extractable: bool,
380    pub encrypt: bool,
381    pub decrypt: bool,
382    pub derive: bool,
383    pub sign: bool,
384    pub verify: bool,
385    pub sign_recover: bool,
386    pub verify_recover: bool,
387    pub wrap: bool,
388    pub unwrap: bool,
389}
390
391impl PrivateKeyRecord {
392    /// The record macOS writes for an RSA private key imported alongside its
393    /// certificate.
394    pub fn for_private_key(label: &str, public_key_hash: [u8; 20], key_size_in_bits: u32) -> Self {
395        Self {
396            key_class: KEY_CLASS_PRIVATE,
397            print_name: label.to_string(),
398            alias: Vec::new(),
399            permanent: true,
400            private: false,
401            modifiable: false,
402            label: public_key_hash,
403            application_tag: Vec::new(),
404            key_creator: ItemKeyRecord::for_item_key([0u8; 20]).key_creator,
405            key_type: KEY_TYPE_RSA,
406            key_size_in_bits,
407            effective_key_size: key_size_in_bits,
408            start_date: vec![0u8; 8],
409            end_date: vec![0u8; 8],
410            sensitive: true,
411            always_sensitive: true,
412            // An imported key is extractable: that is how it got in, and macOS
413            // records it that way.
414            extractable: true,
415            never_extractable: false,
416            encrypt: true,
417            decrypt: true,
418            derive: true,
419            sign: true,
420            verify: true,
421            sign_recover: true,
422            verify_recover: true,
423            wrap: true,
424            unwrap: true,
425        }
426    }
427
428    pub fn to_attributes(&self, relation: &Relation) -> Vec<Option<Value>> {
429        let mut writer = AttributeWriter::new(relation);
430        writer
431            .set("KeyClass", Value::Uint32(self.key_class))
432            .set(
433                "PrintName",
434                Value::Blob(self.print_name.as_bytes().to_vec()),
435            )
436            .set("Alias", Value::Blob(self.alias.clone()))
437            .flag("Permanent", self.permanent)
438            .flag("Private", self.private)
439            .flag("Modifiable", self.modifiable)
440            .set("Label", Value::Blob(self.label.to_vec()))
441            .set("ApplicationTag", Value::Blob(self.application_tag.clone()))
442            .set("KeyCreator", Value::Blob(self.key_creator.clone()))
443            .set("KeyType", Value::Uint32(self.key_type))
444            .set("KeySizeInBits", Value::Uint32(self.key_size_in_bits))
445            .set("EffectiveKeySize", Value::Uint32(self.effective_key_size))
446            .set("StartDate", Value::Blob(self.start_date.clone()))
447            .set("EndDate", Value::Blob(self.end_date.clone()))
448            .flag("Sensitive", self.sensitive)
449            .flag("AlwaysSensitive", self.always_sensitive)
450            .flag("Extractable", self.extractable)
451            .flag("NeverExtractable", self.never_extractable)
452            .flag("Encrypt", self.encrypt)
453            .flag("Decrypt", self.decrypt)
454            .flag("Derive", self.derive)
455            .flag("Sign", self.sign)
456            .flag("Verify", self.verify)
457            .flag("SignRecover", self.sign_recover)
458            .flag("VerifyRecover", self.verify_recover)
459            .flag("Wrap", self.wrap)
460            .flag("Unwrap", self.unwrap);
461        writer.finish()
462    }
463}
464
465/// A date attribute: the timestamp plus the NUL the format includes.
466fn date_field(timestamp: &str) -> Vec<u8> {
467    format!("{timestamp}\0").into_bytes()
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473    use crate::schema::{AttributeDef, AttributeFormat, RecordType};
474
475    fn relation(names: &[&str]) -> Relation {
476        Relation {
477            record_type: RecordType::SYMMETRIC_KEY,
478            name: None,
479            attributes: names
480                .iter()
481                .enumerate()
482                .map(|(index, name)| AttributeDef {
483                    id: index as u32,
484                    name: (*name).to_string(),
485                    format: AttributeFormat::Blob,
486                })
487                .collect(),
488        }
489    }
490
491    #[test]
492    fn key_creator_is_a_braced_guid_string() {
493        let record = ItemKeyRecord::for_item_key([0u8; 20]);
494        assert_eq!(
495            record.key_creator,
496            b"{87191ca2-0fc9-11d4-849a-000502b52122}\0".to_vec(),
497            "must match the string macOS stores"
498        );
499        assert_eq!(record.key_creator.len(), 39);
500    }
501
502    #[test]
503    fn key_record_lowers_every_attribute_the_relation_declares() {
504        let names = [
505            "KeyClass",
506            "PrintName",
507            "Alias",
508            "Permanent",
509            "Private",
510            "Modifiable",
511            "Label",
512            "ApplicationTag",
513            "KeyCreator",
514            "KeyType",
515            "KeySizeInBits",
516            "EffectiveKeySize",
517            "StartDate",
518            "EndDate",
519            "Sensitive",
520            "AlwaysSensitive",
521            "Extractable",
522            "NeverExtractable",
523            "Encrypt",
524            "Decrypt",
525            "Derive",
526            "Sign",
527            "Verify",
528            "SignRecover",
529            "VerifyRecover",
530            "Wrap",
531            "Unwrap",
532        ];
533        let relation = relation(&names);
534        let mut label = [0u8; 20];
535        label[..4].copy_from_slice(b"ssgp");
536        let values = ItemKeyRecord::for_item_key(label).to_attributes(&relation);
537
538        assert_eq!(values.len(), names.len());
539        for (index, name) in names.iter().enumerate() {
540            assert!(
541                values[index].is_some(),
542                "{name} must have a value: securityd asks for it"
543            );
544        }
545        assert_eq!(values[0], Some(Value::Uint32(KEY_CLASS_SYMMETRIC)));
546        assert_eq!(values[1], Some(Value::Blob(label.to_vec())));
547        assert_eq!(
548            values[2],
549            Some(Value::Blob(Vec::new())),
550            "Alias is present but empty"
551        );
552        assert_eq!(
553            values[12],
554            Some(Value::Blob(vec![0u8; 8])),
555            "unset dates are eight zeroes"
556        );
557    }
558
559    #[test]
560    fn unknown_attribute_names_are_skipped_not_fatal() {
561        let relation = relation(&["KeyClass", "Label"]);
562        let values = ItemKeyRecord::for_item_key([1u8; 20]).to_attributes(&relation);
563        assert_eq!(values.len(), 2);
564        assert_eq!(values[0], Some(Value::Uint32(KEY_CLASS_SYMMETRIC)));
565    }
566
567    #[test]
568    fn attributes_macos_always_writes_are_present_even_when_empty() {
569        let relation = relation(&[
570            "cdat",
571            "mdat",
572            "desc",
573            "icmt",
574            "PrintName",
575            "acct",
576            "svce",
577            "gena",
578            "port",
579        ]);
580        let record = PasswordRecord {
581            created: "20260725123456Z".into(),
582            modified: "20260725123456Z".into(),
583            print_name: "myservice".into(),
584            account: Some("alice".into()),
585            service: Some("myservice".into()),
586            ..PasswordRecord::default()
587        };
588        let values = record.to_attributes(&relation);
589
590        assert_eq!(values[0], Some(Value::Date(b"20260725123456Z\0".to_vec())));
591        // desc, icmt and gena are stored empty rather than omitted; an item that
592        // omits them is invisible to the Security framework.
593        assert_eq!(values[2], Some(Value::Blob(Vec::new())));
594        assert_eq!(values[3], Some(Value::Blob(Vec::new())));
595        assert_eq!(values[7], Some(Value::Blob(Vec::new())));
596        assert_eq!(values[4], Some(Value::Blob(b"myservice".to_vec())));
597        assert_eq!(values[5], Some(Value::Blob(b"alice".to_vec())));
598        // A port is genuinely absent on a generic item.
599        assert_eq!(values[8], None);
600    }
601
602    #[test]
603    fn class_specific_always_present_attributes_follow_the_relation() {
604        // An internet relation has sdmn but no gena, and vice versa.
605        let internet = relation(&["cdat", "mdat", "PrintName", "sdmn", "srvr"]);
606        let values = PasswordRecord {
607            server: Some("h".into()),
608            ..PasswordRecord::default()
609        }
610        .to_attributes(&internet);
611        assert_eq!(
612            values[3],
613            Some(Value::Blob(Vec::new())),
614            "sdmn present but empty"
615        );
616
617        let generic = relation(&["cdat", "mdat", "PrintName", "gena", "svce"]);
618        let values = PasswordRecord {
619            service: Some("s".into()),
620            ..PasswordRecord::default()
621        }
622        .to_attributes(&generic);
623        assert_eq!(
624            values[3],
625            Some(Value::Blob(Vec::new())),
626            "gena present but empty"
627        );
628    }
629
630    #[test]
631    fn internet_attributes_are_encoded_in_their_formats() {
632        let relation = relation(&[
633            "cdat",
634            "mdat",
635            "PrintName",
636            "srvr",
637            "path",
638            "port",
639            "ptcl",
640            "atyp",
641        ]);
642        let record = PasswordRecord {
643            created: "20260725123456Z".into(),
644            modified: "20260725123456Z".into(),
645            print_name: "example.com".into(),
646            server: Some("example.com".into()),
647            path: Some("/login".into()),
648            port: Some(8080),
649            protocol: Some(*b"http"),
650            auth_type: Some(*b"dflt"),
651            ..PasswordRecord::default()
652        };
653        let values = record.to_attributes(&relation);
654
655        assert_eq!(values[5], Some(Value::Uint32(8080)));
656        // The protocol is an integer holding a four-char code; the auth type is
657        // the same code as bytes. That asymmetry is the format's, not a slip.
658        assert_eq!(values[6], Some(Value::Uint32(u32::from_be_bytes(*b"http"))));
659        assert_eq!(values[7], Some(Value::Blob(b"dflt".to_vec())));
660    }
661}