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
« 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#
6import base64
7import json
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
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
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 }
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']
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)
68 for key in ['group', 'type', 'icon']:
69 entry[key] = str(item.get(key, '')).lower()
70 self.data.append(entry)
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 }
98 def decrypt(self, jsons):
99 """Import file is AES GCM encrypted, let's decrypt it.
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')
109 password = getpassword(self.prefix)
110 master_key = None
111 for slot in jsons['header']['slots']:
112 if slot['type'] != 1:
113 continue
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"))
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
134 if master_key is None: # pragma: no cover
135 raise FormatError("unable to decrypt the master key.")
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')
145 def parse(self):
146 """Parse Aegis encrypted JSON file."""
147 self.content = self.decrypt(json.loads(self.content))
148 super().parse()
151register_managers(Aegis, AegisCipher)