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

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# 

6 

7import re 

8from datetime import datetime 

9 

10try: 

11 from defusedxml import ElementTree 

12except ImportError: 

13 from xml.etree import ElementTree 

14 

15import yaml 

16from pass_import.core import Cap, register_detecters, register_managers 

17from pass_import.detecter import Formatter 

18from pass_import.manager import PasswordImporter 

19 

20 

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 

47 

48 # Import methods 

49 

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 

69 

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 

88 

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 

99 

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 

107 

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, '' 

115 

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, '' 

123 

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 

130 

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 

142 

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, ''))) 

148 

149 self.data.append(entry) 

150 

151 # Format recognition methods 

152 

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 

163 

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 

172 

173 @classmethod 

174 def header(cls): 

175 """Get keychain format header.""" 

176 return cls.keychain_format 

177 

178 

179register_managers(AppleKeychain) 

180register_detecters(AppleKeychain)