Coverage for pass_import/managers/aegis.py: 100%

66 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# 

5 

6import base64 

7import json 

8 

9try: 

10 from cryptography.exceptions import InvalidTag 

11 from cryptography.hazmat.backends import default_backend 

12 from cryptography.hazmat.primitives.ciphers.aead import AESGCM 

13 from cryptography.hazmat.primitives.kdf.scrypt import Scrypt 

14 CRYPTOGRAPHY = True 

15except ImportError: 

16 CRYPTOGRAPHY = False 

17 

18from pass_import.core import register_managers 

19from pass_import.errors import FormatError 

20from pass_import.formats.otp import OTP 

21from pass_import.tools import getpassword 

22 

23 

24class Aegis(OTP): 

25 """Importer for Aegis otp plain JSON format.""" 

26 name = 'aegis' 

27 format = 'json' 

28 url = 'https://github.com/beemdevelopment/Aegis' 

29 hexport = 'Settings> Tools: Export Plain' 

30 himport = 'pass import aegis file.json' 

31 json_header = { 

32 'version': 1, 

33 'header': { 

34 'slots': None, 

35 'params': None, 

36 }, 

37 'db': { 

38 'version': 1, 

39 'entries': [{ 

40 'type': str, 

41 'uuid': str, 

42 'name': str, 

43 'issuer': str, 

44 'info': { 

45 'secret': str, 

46 'algo': str, 

47 'digits': int 

48 } 

49 }] 

50 } 

51 } 

52 

53 def parse(self): 

54 """Parse Aegis plain JSON file.""" 

55 self.content = json.loads(self.content) 

56 if 'db' in self.content: 

57 self.content = self.content['db'] 

58 

59 for item in self.content.get('entries', []): 

60 entry = {} 

61 info = item.pop('info', {}) 

62 item.update(info) 

63 item['algorithm'] = item.pop('algo', None) 

64 entry['title'] = item['issuer'] + item['name'] 

65 item['label'] = entry['title'] 

66 entry['otpauth'] = self._otp(item) 

67 

68 for key in ['group', 'type', 'icon']: 

69 entry[key] = str(item.get(key, '')).lower() 

70 self.data.append(entry) 

71 

72 

73class AegisCipher(Aegis): 

74 """Importer for Aegis otp encrypted JSON format.""" 

75 name = 'aegis' 

76 format = 'json' 

77 default = False 

78 url = 'https://github.com/beemdevelopment/Aegis' 

79 hexport = 'Settings> Tools: Export encrypted' 

80 himport = 'pass import aegis file.json' 

81 json_header = { 

82 'version': 1, 

83 'header': { 

84 'slots': [{ 

85 'type': int, 

86 'uuid': str, 

87 'key': str, 

88 'key_params': dict 

89 }], 

90 'params': { 

91 'nonce': str, 

92 'tag': str 

93 }, 

94 }, 

95 'db': str, 

96 } 

97 

98 def decrypt(self, jsons): 

99 """Import file is AES GCM encrypted, let's decrypt it. 

100 

101 Based on the import script from Aegis: 

102 https://github.com/beemdevelopment/Aegis/blob/master/scripts/decrypt.py 

103 Format documentation: 

104 https://github.com/beemdevelopment/Aegis/blob/master/docs/vault.md 

105 """ 

106 if not CRYPTOGRAPHY: 

107 raise ImportError(name='cryptography') 

108 

109 password = getpassword(self.prefix) 

110 master_key = None 

111 for slot in jsons['header']['slots']: 

112 if slot['type'] != 1: 

113 continue 

114 

115 kdf = Scrypt(salt=bytes.fromhex(slot['salt']), 

116 length=32, 

117 n=slot['n'], 

118 r=slot['r'], 

119 p=slot['p'], 

120 backend=default_backend()) 

121 key = kdf.derive(password.encode("utf-8")) 

122 

123 cipher = AESGCM(key) 

124 param = slot['key_params'] 

125 try: 

126 nonce = bytes.fromhex(param['nonce']) 

127 data = bytes.fromhex(slot['key']) + bytes.fromhex(param['tag']) 

128 master_key = cipher.decrypt(nonce=nonce, 

129 data=data, 

130 associated_data=None) 

131 except InvalidTag: # pragma: no cover 

132 pass 

133 

134 if master_key is None: # pragma: no cover 

135 raise FormatError("unable to decrypt the master key.") 

136 

137 cipher = AESGCM(master_key) 

138 param = jsons['header']['params'] 

139 content = base64.b64decode(jsons['db']) + bytes.fromhex(param['tag']) 

140 plain = cipher.decrypt(nonce=bytes.fromhex(param['nonce']), 

141 data=content, 

142 associated_data=None) 

143 return plain.decode('utf-8') 

144 

145 def parse(self): 

146 """Parse Aegis encrypted JSON file.""" 

147 self.content = self.decrypt(json.loads(self.content)) 

148 super().parse() 

149 

150 

151register_managers(Aegis, AegisCipher)