Coverage for pass_import/managers/applekeychain.py: 99%
105 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-02-26 12:11 +0000
1# -*- encoding: utf-8 -*-
2# pass import - Passwords importer swiss army knife
3# Copyright (C) 2017-2024 Alexandre PUJOL <alexandre@pujol.io>.
4# Copyright (C) 2019 Santi González https://github.com/santigz
5#
7import re
8from datetime import datetime
10try:
11 from defusedxml import ElementTree
12except ImportError:
13 from xml.etree import ElementTree
15import yaml
16from pass_import.core import Cap, register_detecters, register_managers
17from pass_import.detecter import Formatter
18from pass_import.manager import PasswordImporter
21class AppleKeychain(Formatter, PasswordImporter):
22 """Importer for Apple Keychain."""
23 cap = Cap.FORMAT | Cap.IMPORT
24 name = 'apple-keychain'
25 format = 'keychain'
26 url = 'https://support.apple.com/guide/keychain-access'
27 hexport = ('See this guide: https://gist.github.com/santigz/'
28 '601f4fd2f039d6ceb2198e2f9f4f01e0')
29 himport = 'pass import applekeychain file.txt'
30 keychain_format = ['version', 'class', 'data', 'attributes']
31 keys = {
32 'title': 7,
33 'login': 'acct',
34 'authentication_type': 'atyp',
35 'creation_date': 'cdat',
36 'creator': 'crtr',
37 'description': 'desc',
38 'alt_comment': 'crtr',
39 'modification_date': 'mdat',
40 'password_path': 'path',
41 'protocol': 'ptcl',
42 'url': 'srvr',
43 'security_domain': 'sdmn',
44 'service': 'svce'
45 }
46 yamls = None
48 # Import methods
50 @staticmethod
51 def keychain2yaml(file):
52 """Convert keychain to yaml."""
53 yamls = []
54 data = file.read()
55 characters = {
56 'data:\n': 'data: ',
57 '<NULL>': '',
58 r'<[\w]*>=': ': ',
59 '0x00000007 :': '0x00000007:',
60 '0x00000008 :': '0x00000008:',
61 'keychain: "([^"]*)"': '---'
62 }
63 for key, value in characters.items():
64 data = re.sub(key, value, data)
65 data = data.strip('---').split('---')
66 for block in data:
67 yamls.append(yaml.safe_load(block))
68 return yamls
70 @staticmethod
71 def _compose_url(entry):
72 """Compose the URL from Apple non-standard protocol names."""
73 sub = {
74 'htps': 'https',
75 'ldps': 'ldaps',
76 'ntps': 'nntps',
77 'sox': 'socks',
78 'teln': 'telnet',
79 'tels': 'telnets',
80 'imps': 'imaps',
81 'pops': 'pop3s'
82 }
83 url = entry.get('url', '')
84 protocol = entry.get('protocol', '')
85 if url and protocol:
86 url = f"{sub.get(protocol, protocol)}://{url.strip()}"
87 return url
89 @staticmethod
90 def _human_date(date):
91 """Return the date in human readable format."""
92 try:
93 if date[-1:] == '\x00':
94 date = date[:-1]
95 thedate = datetime.strptime(date, '%Y%m%d%H%M%SZ')
96 return str(thedate)
97 except (ValueError, UnicodeError): # pragma: no cover
98 return date
100 @staticmethod
101 def _decode(string):
102 """Extract and decode hexadecimal value from a string."""
103 hexmod = re.findall(r'0x[0-9A-F]*', string)
104 if hexmod:
105 return bytes.fromhex(hexmod[0][2:]).decode('utf-8')
106 return string
108 def _decode_data(self, entry):
109 """Decode data field (password or comments)."""
110 key = entry.get('type', 'password')
111 key = 'comments' if key == 'note' else key
112 data = entry.pop('data', '')
113 if isinstance(data, int):
114 return key, ''
116 data = self._decode(data)
117 if key == 'comments':
118 if data:
119 try:
120 tree = ElementTree.XML(data)
121 except ElementTree.ParseError:
122 return key, ''
124 found = tree.find('.//string')
125 if found is None:
126 return key, ''
127 return key, found.text
128 return key, ''
129 return key, data
131 def parse(self):
132 """Parse apple-keychain format by converting it in yaml first."""
133 yamls = self.keychain2yaml(self.file)
134 keys = self.invkeys()
135 for block in yamls:
136 entry = {}
137 attributes = block.pop('attributes', {})
138 block.update(attributes)
139 for key, value in block.items():
140 if value is not None:
141 entry[keys.get(key, key)] = value
143 key, value = self._decode_data(entry)
144 entry[key] = value
145 entry['url'] = self._compose_url(entry)
146 for key in ['creation_date', 'modification_date']:
147 entry[key] = self._human_date(self._decode(entry.get(key, '')))
149 self.data.append(entry)
151 # Format recognition methods
153 def is_format(self):
154 """Check keychain file format."""
155 try:
156 self.yamls = self.keychain2yaml(self.file)
157 if isinstance(self.yamls, str):
158 return False
159 except (yaml.scanner.ScannerError, yaml.parser.ParserError,
160 UnicodeDecodeError):
161 return False
162 return True
164 def checkheader(self, header, only=False):
165 """Check keychain format."""
166 if isinstance(self.yamls, list):
167 self.yamls = self.yamls[0]
168 for yamlkey in header:
169 if yamlkey not in self.yamls:
170 return False
171 return True
173 @classmethod
174 def header(cls):
175 """Get keychain format header."""
176 return cls.keychain_format
179register_managers(AppleKeychain)
180register_detecters(AppleKeychain)