use super::{error::LexError, Error, Result};
use logos::{Lexer, Logos};
use std::{borrow::Cow, collections::HashMap, ops::Range};
const NOTE_TYPE: &str = "note";
const NOTE_PLIST_KEY: &str = "NOTE";
type LexResult<T> = std::result::Result<T, LexError>;
#[derive(Logos, Debug, PartialEq)]
enum OctalToken {
#[regex("\\\\(\\d\\d\\d)")]
OctalEscape,
}
pub fn unescape_octal(value: &str) -> Result<Cow<'_, str>> {
let mut lex = OctalToken::lexer(value);
let mut has_escape = false;
let mut tokens = Vec::new();
while let Some(token) = lex.next() {
if let Ok(OctalToken::OctalEscape) = token {
has_escape = true;
}
let span = lex.span();
tokens.push((token, span));
}
if !has_escape {
Ok(Cow::Borrowed(value))
} else {
let mut s = String::new();
for (token, span) in tokens {
if let Ok(OctalToken::OctalEscape) = token {
let octal = &value[span.start + 1..span.end];
let num = u32::from_str_radix(octal, 8)?;
s.push(char::from_u32(num).ok_or_else(|| {
Error::InvalidOctalEscape(
value[span.start..span.end].to_owned(),
)
})?);
} else {
s.push_str(&value[span]);
}
}
Ok(Cow::Owned(s))
}
}
pub fn plist_secure_note(
value: &str,
unescape: bool,
) -> Result<Option<Cow<str>>> {
let plist = if unescape {
unescape_octal(value)?
} else {
Cow::Borrowed(value)
};
let value: plist::Value = plist::from_bytes(plist.as_bytes())?;
if let plist::Value::Dictionary(map) = value {
if let Some(plist::Value::String(data)) = map.get(NOTE_PLIST_KEY) {
return Ok(Some(Cow::Owned(data.to_owned())));
}
}
Ok(None)
}
#[derive(Logos, Debug, PartialEq, Copy, Clone)]
#[logos(error = LexError)]
enum Token {
#[token("keychain:")]
Keychain,
#[token("version:")]
Version,
#[token("class:")]
Class,
#[token("attributes:")]
Attributes,
#[token("data:")]
Data,
#[token("=")]
Equality,
#[token("\"")]
DoubleQuote,
#[regex("(?i:0x[a-f0-9]+)")]
HexValue,
#[regex("\\d+")]
Number,
#[token("<NULL>")]
Null,
#[regex("<(blob|timedate|uint32|sint32)>")]
Type,
#[regex(r"[ \t\r\n\f]+")]
WhiteSpace,
}
pub struct KeychainParser<'s> {
source: &'s str,
}
impl<'s> KeychainParser<'s> {
pub fn new(source: &'s str) -> Self {
Self { source }
}
fn lex(&self) -> Lexer<'s, Token> {
Token::lexer(self.source)
}
pub fn parse(&self) -> Result<KeychainList<'s>> {
let mut entries: Vec<KeychainEntry<'s>> = Vec::new();
let mut lex = self.lex();
let mut in_attributes = false;
let mut next_token = lex.next();
while let Some(token) = next_token {
let token = token?;
match token {
Token::Keychain => {
in_attributes = false;
let advance_token = Self::consume_whitespace(&mut lex);
let range = Self::parse_quoted_string(
&mut lex,
self.source,
advance_token,
)?;
let entry = KeychainEntry {
keychain: &self.source[range],
version: None,
class: None,
data: None,
attributes: HashMap::new(),
};
entries.push(entry);
}
Token::Version => {
let token = Self::consume_whitespace(&mut lex);
let range =
Self::parse_number(&mut lex, self.source, token)?;
if let Some(last) = entries.last_mut() {
last.version = Some(&self.source[range]);
}
}
Token::Class => {
let token = Self::consume_whitespace(&mut lex);
let range = Self::parse_quoted_string(
&mut lex,
self.source,
token,
)?;
if let Some(last) = entries.last_mut() {
let class = &self.source[range];
last.class = Some(class.try_into()?);
}
}
Token::Attributes => {
in_attributes = true;
let token = Self::consume_whitespace(&mut lex);
next_token = token;
continue;
}
Token::Data => {
in_attributes = false;
let token = Self::consume_whitespace(&mut lex);
if let Some(Ok(Token::Keychain)) = token {
next_token = token;
continue;
}
let value =
Self::parse_value(&mut lex, self.source, token)?;
if let Some(last) = entries.last_mut() {
last.data = Some(value);
}
}
_ => {
if in_attributes {
let range = Self::parse_attribute_name(
&mut lex,
self.source,
Some(Ok(token)),
)?;
let name = &self.source[range];
let name: AttributeName = name.try_into()?;
let token = Self::consume_whitespace(&mut lex);
let range = Self::parse_attribute_type(
&mut lex,
self.source,
token,
)?;
let attr_type = &self.source[range];
let attr_type: AttributeType =
attr_type.try_into()?;
let equals = lex.next();
if !matches!(equals, Some(Ok(Token::Equality))) {
return Err(Error::ParseExpectsEquals);
}
let value = Self::parse_attribute_value(
&mut lex,
self.source,
&attr_type,
)?;
if let Some(last) = entries.last_mut() {
let key = AttributeKey(name, attr_type);
last.attributes.insert(key, value);
}
let token = Self::consume_whitespace(&mut lex);
next_token = token;
continue;
}
}
}
next_token = lex.next();
}
Ok(KeychainList { entries })
}
fn consume_whitespace(
lex: &mut Lexer<Token>,
) -> Option<LexResult<Token>> {
lex.by_ref().find(|t| !matches!(t, Ok(Token::WhiteSpace)))
}
fn parse_quoted_string(
lex: &mut Lexer<Token>,
source: &str,
mut next_token: Option<LexResult<Token>>,
) -> Result<Range<usize>> {
let mut in_quote = false;
let mut begin: Range<usize> = lex.span();
while let Some(token) = next_token {
match token {
Ok(Token::HexValue) => {
if !in_quote {
return Ok(lex.span());
}
}
Ok(Token::DoubleQuote) => {
if !in_quote {
begin = lex.span();
in_quote = true;
} else {
let finished = lex.remainder().is_empty()
|| &lex.remainder()[0..1] == "\n";
if finished {
return Ok(begin.end..lex.span().start);
}
}
}
_ => {}
}
next_token = lex.next();
}
Err(Error::ParseNotQuoted(source[lex.span()].to_owned()))
}
fn parse_attribute_name(
lex: &mut Lexer<Token>,
source: &str,
mut next_token: Option<LexResult<Token>>,
) -> Result<Range<usize>> {
while let Some(token) = next_token {
match token? {
Token::HexValue => {
return Ok(lex.span());
}
Token::DoubleQuote => {
let start = lex.span().end;
let remainder = lex.remainder();
if remainder.len() >= 4 {
lex.bump(4);
}
let end_quote = lex.next();
if !matches!(end_quote, Some(Ok(Token::DoubleQuote))) {
return Err(Error::ParseAttributeNameQuote(
source[lex.span()].to_owned(),
));
}
return Ok(start..lex.span().start);
}
_ => {}
}
next_token = lex.next();
}
Err(Error::ParseNotAttributeName(source[lex.span()].to_owned()))
}
fn parse_attribute_type(
lex: &mut Lexer<Token>,
source: &str,
mut next_token: Option<LexResult<Token>>,
) -> Result<Range<usize>> {
while let Some(token) = next_token {
if let Token::Type = token? {
return Ok(lex.span());
}
next_token = lex.next();
}
Err(Error::ParseNotAttributeType(source[lex.span()].to_owned()))
}
fn parse_attribute_value<'a>(
lex: &mut Lexer<Token>,
source: &'a str,
_attr_type: &AttributeType,
) -> Result<Value<'a>> {
let token = lex.next();
Self::parse_value(lex, source, token)
}
fn parse_value<'a>(
lex: &mut Lexer<Token>,
source: &'a str,
token: Option<LexResult<Token>>,
) -> Result<Value<'a>> {
if let Some(token) = token {
let token = token?;
match token {
Token::Null => return Ok(Value::Null),
Token::HexValue => {
let hex = &source[lex.span()];
if lex.remainder().starts_with(r#" ""#) {
let next_token = lex.next();
let range = Self::parse_quoted_string(
lex, source, next_token,
)?;
let value = &source[range];
return Ok(Value::BlobString(hex, value));
}
return Ok(Value::Blob(hex));
}
Token::DoubleQuote => {
let range = Self::parse_quoted_string(
lex,
source,
Some(Ok(token)),
)?;
let value = &source[range];
return Ok(Value::String(value));
}
_ => {
return Err(Error::ParseValue(
source[lex.span()].to_owned(),
))
}
}
}
Err(Error::ParseValue(source[lex.span()].to_owned()))
}
fn parse_number(
lex: &mut Lexer<Token>,
source: &str,
mut next_token: Option<LexResult<Token>>,
) -> Result<Range<usize>> {
while let Some(token) = next_token {
if let Token::Number = token? {
return Ok(lex.span());
}
next_token = lex.next();
}
Err(Error::ParseNotNumber(source[lex.span()].to_owned()))
}
}
#[derive(Debug)]
pub struct KeychainList<'s> {
entries: Vec<KeychainEntry<'s>>,
}
impl<'s> KeychainList<'s> {
pub fn entries(&self) -> &[KeychainEntry<'s>] {
self.entries.as_slice()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.len() == 0
}
pub fn find_generic_password(
&self,
service: &str,
account: &str,
) -> Option<&KeychainEntry<'_>> {
self.entries.iter().find(|entry| {
if let Some(EntryClass::GenericPassword) = entry.class {
if let (Some((_, attr_service)), Some((_, attr_account))) = (
entry.find_attribute_by_name(
AttributeName::SecServiceItemAttr,
),
entry.find_attribute_by_name(
AttributeName::SecAccountItemAttr,
),
) {
if attr_service.matches(service)
&& attr_account.matches(account)
{
return true;
}
}
}
false
})
}
pub fn find_generic_note(
&self,
service: &str,
) -> Option<&KeychainEntry<'_>> {
self.entries.iter().find(|entry| {
if let Some(EntryClass::GenericPassword) = entry.class {
if let (Some((_, attr_service)), Some((_, attr_type))) = (
entry.find_attribute_by_name(
AttributeName::SecServiceItemAttr,
),
entry.find_attribute_by_name(
AttributeName::SecTypeItemAttr,
),
) {
if attr_service.matches(service)
&& attr_type.matches(NOTE_TYPE)
{
return true;
}
}
}
false
})
}
}
#[derive(Debug)]
pub struct KeychainEntry<'s> {
#[allow(dead_code)]
keychain: &'s str,
version: Option<&'s str>,
class: Option<EntryClass>,
attributes: HashMap<AttributeKey<'s>, Value<'s>>,
data: Option<Value<'s>>,
}
impl<'s> KeychainEntry<'s> {
pub fn data(&self) -> Option<&Value<'s>> {
self.data.as_ref()
}
pub fn find_attribute_by_name(
&self,
name: AttributeName<'_>,
) -> Option<(&AttributeType, &Value<'_>)> {
self.attributes.iter().find_map(|(key, value)| {
if key.0 == name {
Some((&key.1, value))
} else {
None
}
})
}
pub fn is_note(&self) -> bool {
let type_attr =
self.find_attribute_by_name(AttributeName::SecTypeItemAttr);
if let Some((_, attr_type)) = type_attr {
return attr_type.matches(NOTE_TYPE);
}
false
}
pub fn generic_data(&self) -> Result<Option<Cow<str>>> {
if let Some(data) = &self.data {
if let Some(EntryClass::GenericPassword) = self.class {
if self.is_note() {
if let Value::BlobString(_, value) = data {
return plist_secure_note(value, true);
}
} else {
match data {
Value::String(value) => {
return Ok(Some(Cow::Borrowed(value)))
}
Value::BlobString(_, value) => {
return Ok(Some(Cow::Borrowed(value)))
}
_ => {}
}
}
}
}
Ok(None)
}
}
#[derive(Debug)]
pub enum EntryClass {
GenericPassword,
InternetPassword,
AppleSharePassword,
Certificate,
PublicKey,
PrivateKey,
SymmetricKey,
}
impl TryFrom<&str> for EntryClass {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
match value {
"genp" => Ok(Self::GenericPassword),
"inet" => Ok(Self::InternetPassword),
"ashp" => Ok(Self::AppleSharePassword),
"0x80001000" => Ok(Self::Certificate),
"0x0000000F" => Ok(Self::PublicKey),
"0x00000010" => Ok(Self::PrivateKey),
"0x00000011" => Ok(Self::SymmetricKey),
_ => Err(Error::ParseUnknownClass(value.to_owned())),
}
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum AttributeName<'s> {
Hex(&'s str),
SecCreationDateItemAttr,
SecModDateItemAttr,
SecDescriptionItemAttr,
SecCommentItemAttr,
SecCreatorItemAttr,
SecTypeItemAttr,
SecScriptCodeItemAttr,
SecLabelItemAttr,
SecInvisibleItemAttr,
SecNegativeItemAttr,
SecCustomIconItemAttr,
SecAccountItemAttr,
SecServiceItemAttr,
SecGenericItemAttr,
SecSecurityDomainItemAttr,
SecServerItemAttr,
SecAuthenticationTypeItemAttr,
SecPortItemAttr,
SecPathItemAttr,
SecVolumeItemAttr,
SecAddressItemAttr,
SecSignatureItemAttr,
SecProtocolItemAttr,
SecCertificateType,
SecCertificateEncoding,
SecCrlType,
SecCrlEncoding,
SecAlias,
Unknown(&'s str),
}
impl<'s> TryFrom<&'s str> for AttributeName<'s> {
type Error = Error;
fn try_from(value: &'s str) -> Result<Self> {
match value {
"cdat" => Ok(Self::SecCreationDateItemAttr),
"mdat" => Ok(Self::SecModDateItemAttr),
"desc" => Ok(Self::SecDescriptionItemAttr),
"icmt" => Ok(Self::SecCommentItemAttr),
"crtr" => Ok(Self::SecCreatorItemAttr),
"type" => Ok(Self::SecTypeItemAttr),
"scrp" => Ok(Self::SecScriptCodeItemAttr),
"labl" => Ok(Self::SecLabelItemAttr),
"invi" => Ok(Self::SecInvisibleItemAttr),
"nega" => Ok(Self::SecNegativeItemAttr),
"cusi" => Ok(Self::SecCustomIconItemAttr),
"acct" => Ok(Self::SecAccountItemAttr),
"svce" => Ok(Self::SecServiceItemAttr),
"gena" => Ok(Self::SecGenericItemAttr),
"sdmn" => Ok(Self::SecSecurityDomainItemAttr),
"srvr" => Ok(Self::SecServerItemAttr),
"atyp" => Ok(Self::SecAuthenticationTypeItemAttr),
"port" => Ok(Self::SecPortItemAttr),
"path" => Ok(Self::SecPathItemAttr),
"vlme" => Ok(Self::SecVolumeItemAttr),
"addr" => Ok(Self::SecAddressItemAttr),
"ssig" => Ok(Self::SecSignatureItemAttr),
"ptcl" => Ok(Self::SecProtocolItemAttr),
"ctyp" => Ok(Self::SecCertificateType),
"cenc" => Ok(Self::SecCertificateEncoding),
"crtp" => Ok(Self::SecCrlType),
"crnc" => Ok(Self::SecCrlEncoding),
"alis" => Ok(Self::SecAlias),
"prot" => Ok(Self::Unknown(value)),
"hpky" => Ok(Self::Unknown(value)),
"issu" => Ok(Self::Unknown(value)),
"skid" => Ok(Self::Unknown(value)),
"snbr" => Ok(Self::Unknown(value)),
"subj" => Ok(Self::Unknown(value)),
_ => {
if value.starts_with("0x") {
Ok(Self::Hex(value))
} else {
Err(Error::ParseUnknownAttributeName(value.to_string()))
}
}
}
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum AttributeType {
Blob,
Uint32,
Sint32,
TimeDate,
}
impl TryFrom<&str> for AttributeType {
type Error = Error;
fn try_from(value: &str) -> Result<Self> {
match value {
"<blob>" => Ok(Self::Blob),
"<uint32>" => Ok(Self::Uint32),
"<sint32>" => Ok(Self::Sint32),
"<timedate>" => Ok(Self::TimeDate),
_ => Err(Error::ParseUnknownAttributeType(value.to_owned())),
}
}
}
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct AttributeKey<'s>(pub AttributeName<'s>, pub AttributeType);
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum Value<'s> {
Null,
TimeDate(&'s str),
String(&'s str),
Uint32(&'s str),
Sint32(&'s str),
BlobString(&'s str, &'s str),
Blob(&'s str),
}
impl<'s> Value<'s> {
pub fn as_str(&self) -> &str {
match *self {
Self::Null => "",
Self::TimeDate(value) => value,
Self::String(value) => value,
Self::Uint32(value) => value,
Self::Sint32(value) => value,
Self::BlobString(_, value) => value,
Self::Blob(value) => value,
}
}
pub fn matches(&self, input: &str) -> bool {
match *self {
Self::Null => false,
Self::TimeDate(value) => value == input,
Self::String(value) => value == input,
Self::Uint32(value) => value == input,
Self::Sint32(value) => value == input,
Self::BlobString(_, value) => value == input,
Self::Blob(value) => value == input,
}
}
}