Skip to main content

kc/
schema.rs

1//! Relations and attributes — the schema a keychain carries about itself.
2//!
3//! A keychain is a CSSM database: four schema tables describe every other
4//! table's attributes, so a reader does not have to hard-code the layout of
5//! password or key records. Only the four schema relations themselves need
6//! built-in definitions, to bootstrap.
7//!
8//! Two things matter and are not obvious:
9//!
10//! * A record's attribute-offset array follows the order attributes are
11//!   *defined in the schema table*, not attribute-id order. Sorting by id
12//!   silently misreads every password record.
13//! * Password relations name most attributes by four-char code in the
14//!   `AttributeID` (`acct`, `svce`, `srvr`), leaving `AttributeName` empty;
15//!   `PrintName` and `Alias` are the exceptions, named by string.
16
17use std::collections::BTreeMap;
18
19use crate::error::{Error, Result};
20use crate::format::{Record, Table, Value, trim_nul};
21
22/// A CSSM relation identifier (`CSSM_DB_RECORDTYPE`).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub struct RecordType(pub u32);
25
26impl RecordType {
27    pub const SCHEMA_INFO: Self = Self(0x0000_0000);
28    pub const SCHEMA_INDEXES: Self = Self(0x0000_0001);
29    pub const SCHEMA_ATTRIBUTES: Self = Self(0x0000_0002);
30    pub const SCHEMA_PARSING_MODULE: Self = Self(0x0000_0003);
31
32    pub const CERT: Self = Self(0x0000_000b);
33    pub const CRL: Self = Self(0x0000_000c);
34    pub const POLICY: Self = Self(0x0000_000d);
35    pub const GENERIC: Self = Self(0x0000_000e);
36    pub const PUBLIC_KEY: Self = Self(0x0000_000f);
37    pub const PRIVATE_KEY: Self = Self(0x0000_0010);
38    pub const SYMMETRIC_KEY: Self = Self(0x0000_0011);
39
40    pub const GENERIC_PASSWORD: Self = Self(0x8000_0000);
41    pub const INTERNET_PASSWORD: Self = Self(0x8000_0001);
42    pub const APPLESHARE_PASSWORD: Self = Self(0x8000_0002);
43    pub const USER_TRUST: Self = Self(0x8000_0003);
44    pub const X509_CRL: Self = Self(0x8000_0004);
45    pub const UNLOCK_REFERRAL: Self = Self(0x8000_0005);
46    pub const EXTENDED_ATTRIBUTE: Self = Self(0x8000_0006);
47    pub const X509_CERTIFICATE: Self = Self(0x8000_1000);
48    pub const METADATA: Self = Self(0x8000_8000);
49
50    /// True for application-specific relations, which set the high bit.
51    pub fn is_application_specific(self) -> bool {
52        self.0 & 0x8000_0000 != 0
53    }
54
55    pub fn name(self) -> &'static str {
56        match self {
57            Self::SCHEMA_INFO => "CSSM_DL_DB_SCHEMA_INFO",
58            Self::SCHEMA_INDEXES => "CSSM_DL_DB_SCHEMA_INDEXES",
59            Self::SCHEMA_ATTRIBUTES => "CSSM_DL_DB_SCHEMA_ATTRIBUTES",
60            Self::SCHEMA_PARSING_MODULE => "CSSM_DL_DB_SCHEMA_PARSING_MODULE",
61            Self::CERT => "CSSM_DL_DB_RECORD_CERT",
62            Self::CRL => "CSSM_DL_DB_RECORD_CRL",
63            Self::POLICY => "CSSM_DL_DB_RECORD_POLICY",
64            Self::GENERIC => "CSSM_DL_DB_RECORD_GENERIC",
65            Self::PUBLIC_KEY => "CSSM_DL_DB_RECORD_PUBLIC_KEY",
66            Self::PRIVATE_KEY => "CSSM_DL_DB_RECORD_PRIVATE_KEY",
67            Self::SYMMETRIC_KEY => "CSSM_DL_DB_RECORD_SYMMETRIC_KEY",
68            Self::GENERIC_PASSWORD => "CSSM_DL_DB_RECORD_GENERIC_PASSWORD",
69            Self::INTERNET_PASSWORD => "CSSM_DL_DB_RECORD_INTERNET_PASSWORD",
70            Self::APPLESHARE_PASSWORD => "CSSM_DL_DB_RECORD_APPLESHARE_PASSWORD",
71            Self::USER_TRUST => "CSSM_DL_DB_RECORD_USER_TRUST",
72            Self::X509_CRL => "CSSM_DL_DB_RECORD_X509_CRL",
73            Self::UNLOCK_REFERRAL => "CSSM_DL_DB_RECORD_UNLOCK_REFERRAL",
74            Self::EXTENDED_ATTRIBUTE => "CSSM_DL_DB_RECORD_EXTENDED_ATTRIBUTE",
75            Self::X509_CERTIFICATE => "CSSM_DL_DB_RECORD_X509_CERTIFICATE",
76            Self::METADATA => "CSSM_DL_DB_RECORD_METADATA",
77            _ => "unknown",
78        }
79    }
80
81    /// Short name for the CLI: `generic`, `internet`, `appleshare`.
82    pub fn short_name(self) -> Option<&'static str> {
83        match self {
84            Self::GENERIC_PASSWORD => Some("generic"),
85            Self::INTERNET_PASSWORD => Some("internet"),
86            Self::APPLESHARE_PASSWORD => Some("appleshare"),
87            _ => None,
88        }
89    }
90}
91
92/// `CSSM_DB_ATTRIBUTE_FORMAT`: how an attribute value is encoded.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum AttributeFormat {
95    String,
96    Sint32,
97    Uint32,
98    BigNum,
99    Real,
100    TimeDate,
101    Blob,
102    MultiUint32,
103    Complex,
104    /// A value this build does not know; treated as a blob.
105    Unknown(u32),
106}
107
108impl AttributeFormat {
109    pub fn from_u32(value: u32) -> Self {
110        match value {
111            0 => Self::String,
112            1 => Self::Sint32,
113            2 => Self::Uint32,
114            3 => Self::BigNum,
115            4 => Self::Real,
116            5 => Self::TimeDate,
117            6 => Self::Blob,
118            7 => Self::MultiUint32,
119            8 => Self::Complex,
120            other => Self::Unknown(other),
121        }
122    }
123
124    pub fn as_u32(self) -> u32 {
125        match self {
126            Self::String => 0,
127            Self::Sint32 => 1,
128            Self::Uint32 => 2,
129            Self::BigNum => 3,
130            Self::Real => 4,
131            Self::TimeDate => 5,
132            Self::Blob => 6,
133            Self::MultiUint32 => 7,
134            Self::Complex => 8,
135            Self::Unknown(other) => other,
136        }
137    }
138}
139
140/// One attribute of a relation.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct AttributeDef {
143    pub id: u32,
144    /// Display name: the schema's string name, else the four-char code of `id`.
145    pub name: String,
146    pub format: AttributeFormat,
147}
148
149#[derive(Debug, Clone)]
150pub struct Relation {
151    pub record_type: RecordType,
152    /// `RelationName` from the schema, when the file records one.
153    pub name: Option<String>,
154    /// Attributes in schema order, which is the record's attribute order.
155    pub attributes: Vec<AttributeDef>,
156}
157
158impl Relation {
159    /// Position of an attribute by name or four-char code, case-insensitively.
160    pub fn index_of(&self, name: &str) -> Option<usize> {
161        self.attributes
162            .iter()
163            .position(|attr| attr.name.eq_ignore_ascii_case(name))
164    }
165}
166
167/// The schema of one keychain.
168#[derive(Debug, Clone, Default)]
169pub struct Schema {
170    relations: BTreeMap<RecordType, Relation>,
171}
172
173impl Schema {
174    /// Built-in definitions for the four schema relations, enough to read the
175    /// schema tables and learn everything else.
176    pub fn bootstrap() -> Self {
177        let uint32 = AttributeFormat::Uint32;
178        let string = AttributeFormat::String;
179        let blob = AttributeFormat::Blob;
180
181        let mut relations = BTreeMap::new();
182        let mut add = |record_type: RecordType, attrs: &[(&str, AttributeFormat)]| {
183            relations.insert(
184                record_type,
185                Relation {
186                    record_type,
187                    name: Some(record_type.name().to_string()),
188                    attributes: attrs
189                        .iter()
190                        .enumerate()
191                        .map(|(index, (name, format))| AttributeDef {
192                            id: index as u32,
193                            name: (*name).to_string(),
194                            format: *format,
195                        })
196                        .collect(),
197                },
198            );
199        };
200
201        add(
202            RecordType::SCHEMA_INFO,
203            &[("RelationID", uint32), ("RelationName", string)],
204        );
205        add(
206            RecordType::SCHEMA_INDEXES,
207            &[
208                ("RelationID", uint32),
209                ("IndexID", uint32),
210                ("AttributeID", uint32),
211                ("IndexType", uint32),
212                ("IndexedDataLocation", uint32),
213            ],
214        );
215        add(
216            RecordType::SCHEMA_ATTRIBUTES,
217            &[
218                ("RelationID", uint32),
219                ("AttributeID", uint32),
220                ("AttributeNameFormat", uint32),
221                ("AttributeName", string),
222                ("AttributeNameID", blob),
223                ("AttributeFormat", uint32),
224            ],
225        );
226        add(
227            RecordType::SCHEMA_PARSING_MODULE,
228            &[
229                ("RelationID", uint32),
230                ("AttributeID", uint32),
231                ("ModuleID", blob),
232                ("AddinVersion", string),
233                ("SSID", uint32),
234                ("SubserviceType", uint32),
235            ],
236        );
237
238        Self { relations }
239    }
240
241    /// Read the schema out of a keychain's own schema tables.
242    pub fn from_tables(tables: &[Table]) -> Result<Self> {
243        let bootstrap = Self::bootstrap();
244        let find =
245            |record_type: RecordType| tables.iter().find(|table| table.record_type == record_type);
246
247        // Relation names, when present.
248        let mut names: BTreeMap<RecordType, String> = BTreeMap::new();
249        if let Some(table) = find(RecordType::SCHEMA_INFO) {
250            for record in table.records() {
251                let Some(id) = record.attribute(0).and_then(Value::as_u32) else {
252                    continue;
253                };
254                if let Some(name) = record.attribute(1).and_then(Value::as_bytes) {
255                    let name = String::from_utf8_lossy(trim_nul(name)).into_owned();
256                    if !name.is_empty() {
257                        names.insert(RecordType(id), name);
258                    }
259                }
260            }
261        }
262
263        let attributes_table = find(RecordType::SCHEMA_ATTRIBUTES)
264            .ok_or(Error::MissingTable("CSSM_DL_DB_SCHEMA_ATTRIBUTES"))?;
265
266        let mut relations: BTreeMap<RecordType, Relation> = BTreeMap::new();
267        for record in attributes_table.records() {
268            let relation_id = record
269                .attribute(0)
270                .and_then(Value::as_u32)
271                .ok_or_else(|| Error::format("schema attribute record has no RelationID"))?;
272            let attribute_id = record
273                .attribute(1)
274                .and_then(Value::as_u32)
275                .ok_or_else(|| Error::format("schema attribute record has no AttributeID"))?;
276            let format =
277                AttributeFormat::from_u32(record.attribute(5).and_then(Value::as_u32).unwrap_or(6));
278
279            let named = record
280                .attribute(3)
281                .and_then(Value::as_bytes)
282                .map(|bytes| String::from_utf8_lossy(trim_nul(bytes)).into_owned())
283                .filter(|name| !name.is_empty());
284            let name = named
285                .or_else(|| {
286                    record
287                        .attribute(4)
288                        .and_then(Value::as_bytes)
289                        .map(|bytes| String::from_utf8_lossy(trim_nul(bytes)).into_owned())
290                        .filter(|name| !name.is_empty())
291                })
292                .unwrap_or_else(|| attribute_name_from_id(attribute_id));
293
294            let record_type = RecordType(relation_id);
295            relations
296                .entry(record_type)
297                .or_insert_with(|| Relation {
298                    record_type,
299                    name: names.get(&record_type).cloned(),
300                    attributes: Vec::new(),
301                })
302                .attributes
303                .push(AttributeDef {
304                    id: attribute_id,
305                    name,
306                    format,
307                });
308        }
309
310        // The schema tables describe themselves inconsistently across macOS
311        // versions; the built-in definitions are authoritative for them.
312        for (record_type, relation) in bootstrap.relations {
313            relations.insert(record_type, relation);
314        }
315
316        // Relations with no attribute records still exist as tables.
317        for table in tables {
318            relations
319                .entry(table.record_type)
320                .or_insert_with(|| Relation {
321                    record_type: table.record_type,
322                    name: names.get(&table.record_type).cloned(),
323                    attributes: Vec::new(),
324                });
325        }
326
327        Ok(Self { relations })
328    }
329
330    pub fn relation(&self, record_type: RecordType) -> Option<&Relation> {
331        self.relations.get(&record_type)
332    }
333
334    pub fn relations(&self) -> impl Iterator<Item = &Relation> {
335        self.relations.values()
336    }
337
338    /// Attribute formats in record order. Empty when the relation is unknown,
339    /// which makes such a record parse as having no attributes.
340    pub fn attribute_formats(&self, record_type: RecordType) -> Vec<AttributeFormat> {
341        self.relations
342            .get(&record_type)
343            .map(|relation| relation.attributes.iter().map(|attr| attr.format).collect())
344            .unwrap_or_default()
345    }
346
347    /// Look up a value on a record by attribute name.
348    pub fn attribute<'r>(
349        &self,
350        record_type: RecordType,
351        record: &'r Record,
352        name: &str,
353    ) -> Option<&'r Value> {
354        let index = self.relation(record_type)?.index_of(name)?;
355        record.attribute(index)
356    }
357
358    /// Every named attribute of a record that has a value.
359    pub fn named_attributes<'r>(
360        &self,
361        record_type: RecordType,
362        record: &'r Record,
363    ) -> Vec<(&str, &'r Value)> {
364        let Some(relation) = self.relation(record_type) else {
365            return Vec::new();
366        };
367        relation
368            .attributes
369            .iter()
370            .enumerate()
371            .filter_map(|(index, attr)| {
372                record
373                    .attribute(index)
374                    .map(|value| (attr.name.as_str(), value))
375            })
376            .collect()
377    }
378}
379
380/// Render an attribute id as its four-char code when that is printable.
381fn attribute_name_from_id(id: u32) -> String {
382    let bytes = id.to_be_bytes();
383    if bytes.iter().all(|byte| (0x20..0x7f).contains(byte)) {
384        String::from_utf8_lossy(&bytes).into_owned()
385    } else {
386        format!("attr{id}")
387    }
388}
389
390/// Four-char code as a big-endian integer, for building attribute ids.
391pub const fn four_char_code(code: &[u8; 4]) -> u32 {
392    u32::from_be_bytes(*code)
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398
399    #[test]
400    fn record_types_report_their_names_and_kind() {
401        assert_eq!(
402            RecordType::GENERIC_PASSWORD.name(),
403            "CSSM_DL_DB_RECORD_GENERIC_PASSWORD"
404        );
405        assert!(RecordType::GENERIC_PASSWORD.is_application_specific());
406        assert!(!RecordType::SYMMETRIC_KEY.is_application_specific());
407        assert_eq!(RecordType::INTERNET_PASSWORD.short_name(), Some("internet"));
408        assert_eq!(RecordType::SYMMETRIC_KEY.short_name(), None);
409        assert_eq!(RecordType(0x1234).name(), "unknown");
410    }
411
412    #[test]
413    fn attribute_formats_round_trip() {
414        for value in 0..=8 {
415            assert_eq!(AttributeFormat::from_u32(value).as_u32(), value);
416        }
417        assert_eq!(AttributeFormat::from_u32(99), AttributeFormat::Unknown(99));
418        assert_eq!(AttributeFormat::from_u32(99).as_u32(), 99);
419    }
420
421    #[test]
422    fn bootstrap_schema_describes_the_four_schema_relations() {
423        let schema = Schema::bootstrap();
424        assert_eq!(schema.attribute_formats(RecordType::SCHEMA_INFO).len(), 2);
425        assert_eq!(
426            schema.attribute_formats(RecordType::SCHEMA_INDEXES).len(),
427            5
428        );
429        assert_eq!(
430            schema
431                .attribute_formats(RecordType::SCHEMA_ATTRIBUTES)
432                .len(),
433            6
434        );
435        assert_eq!(
436            schema
437                .attribute_formats(RecordType::SCHEMA_PARSING_MODULE)
438                .len(),
439            6
440        );
441        // Unknown relations yield no attributes rather than a guess.
442        assert!(
443            schema
444                .attribute_formats(RecordType::GENERIC_PASSWORD)
445                .is_empty()
446        );
447    }
448
449    #[test]
450    fn attribute_names_fall_back_to_four_char_codes() {
451        assert_eq!(attribute_name_from_id(four_char_code(b"acct")), "acct");
452        assert_eq!(attribute_name_from_id(four_char_code(b"svce")), "svce");
453        assert_eq!(attribute_name_from_id(0), "attr0");
454        assert_eq!(attribute_name_from_id(7), "attr7");
455    }
456
457    #[test]
458    fn four_char_codes_are_big_endian() {
459        assert_eq!(four_char_code(b"acct"), 0x6163_6374);
460        assert_eq!(four_char_code(b"svce").to_be_bytes(), *b"svce");
461    }
462
463    #[test]
464    fn attribute_lookup_is_case_insensitive() {
465        let relation = Relation {
466            record_type: RecordType::GENERIC_PASSWORD,
467            name: None,
468            attributes: vec![
469                AttributeDef {
470                    id: 0,
471                    name: "acct".into(),
472                    format: AttributeFormat::Blob,
473                },
474                AttributeDef {
475                    id: 1,
476                    name: "PrintName".into(),
477                    format: AttributeFormat::Blob,
478                },
479            ],
480        };
481        assert_eq!(relation.index_of("acct"), Some(0));
482        assert_eq!(relation.index_of("ACCT"), Some(0));
483        assert_eq!(relation.index_of("printname"), Some(1));
484        assert_eq!(relation.index_of("nope"), None);
485    }
486}