[feat] Make EncryptionKey aware of the active address
[keymanager.git] / src / leap / keymanager / migrator.py
1 # -*- coding: utf-8 -*-
2 # migrator.py
3 # Copyright (C) 2015 LEAP
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, either version 3 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 """
19 Document migrator
20 """
21 # XXX: versioning has being added 12/2015 when keymanager was not
22 #      much in use in the wild. We can probably drop support for
23 #      keys without version at some point.
24
25
26 from collections import namedtuple
27 from twisted.internet.defer import gatherResults, succeed
28
29 from leap.keymanager.keys import (
30     TAGS_PRIVATE_INDEX,
31     KEYMANAGER_KEY_TAG,
32     KEYMANAGER_ACTIVE_TAG,
33
34     KEYMANAGER_DOC_VERSION,
35     KEY_ADDRESS_KEY,
36     KEY_UIDS_KEY,
37     KEY_VERSION_KEY,
38     KEY_FINGERPRINT_KEY,
39     KEY_VALIDATION_KEY,
40     KEY_LAST_AUDITED_AT_KEY,
41     KEY_ENCR_USED_KEY,
42     KEY_SIGN_USED_KEY,
43 )
44 from leap.keymanager.validation import ValidationLevels
45
46
47 KEY_ID_KEY = 'key_id'
48
49 KeyDocs = namedtuple("KeyDocs", ['key', 'active'])
50
51
52 class KeyDocumentsMigrator(object):
53     """
54     Migrate old KeyManager Soledad Documents to the newest schema
55     """
56
57     def __init__(self, soledad):
58         self._soledad = soledad
59
60     def migrate(self):
61         deferred_public = self._get_docs(private=False)
62         deferred_public.addCallback(self._migrate_docs)
63
64         deferred_private = self._get_docs(private=True)
65         deferred_private.addCallback(self._migrate_docs)
66
67         return gatherResults([deferred_public, deferred_private])
68
69     def _get_docs(self, private=False):
70         private_value = '1' if private else '0'
71
72         deferred_keys = self._soledad.get_from_index(
73             TAGS_PRIVATE_INDEX,
74             KEYMANAGER_KEY_TAG,
75             private_value)
76         deferred_active = self._soledad.get_from_index(
77             TAGS_PRIVATE_INDEX,
78             KEYMANAGER_ACTIVE_TAG,
79             private_value)
80         return gatherResults([deferred_keys, deferred_active])
81
82     def _migrate_docs(self, (key_docs, active_docs)):
83         def update_keys(keys):
84             deferreds = []
85             for key_id in keys:
86                 key = keys[key_id].key
87                 actives = keys[key_id].active
88
89                 d = self._migrate_actives(key, actives)
90                 deferreds.append(d)
91
92                 d = self._migrate_key(key)
93                 deferreds.append(d)
94             return gatherResults(deferreds)
95
96         d = self._buildKeyDict(key_docs, active_docs)
97         d.addCallback(lambda keydict: self._filter_outdated(keydict))
98         d.addCallback(update_keys)
99
100     def _buildKeyDict(self, keys, actives):
101         keydict = {
102             fp2id(key.content[KEY_FINGERPRINT_KEY]): KeyDocs(key, [])
103             for key in keys}
104
105         deferreds = []
106         for active in actives:
107             if KEY_ID_KEY in active.content:
108                 key_id = active.content[KEY_ID_KEY]
109                 if key_id not in keydict:
110                     d = self._soledad.delete_doc(active)
111                     deferreds.append(d)
112                     continue
113                 keydict[key_id].active.append(active)
114
115         d = gatherResults(deferreds)
116         d.addCallback(lambda _: keydict)
117         return d
118
119     def _filter_outdated(self, keydict):
120         outdated = {}
121         for key_id, docs in keydict.items():
122             if ((docs.key and KEY_VERSION_KEY not in docs.key.content) or
123                     docs.active):
124                 outdated[key_id] = docs
125         return outdated
126
127     def _migrate_actives(self, key, actives):
128         if not key:
129             deferreds = []
130             for active in actives:
131                 d = self._soledad.delete_doc(active)
132                 deferreds.append(d)
133             return gatherResults(deferreds)
134
135         validation = str(ValidationLevels.Weak_Chain)
136         last_audited = 0
137         encr_used = False
138         sign_used = False
139         fingerprint = key.content[KEY_FINGERPRINT_KEY]
140         if len(actives) == 1 and KEY_VERSION_KEY not in key.content:
141             # we can preserve the validation of the key if there is only one
142             # active address for the key
143             validation = key.content[KEY_VALIDATION_KEY]
144             last_audited = key.content[KEY_LAST_AUDITED_AT_KEY]
145             encr_used = key.content[KEY_ENCR_USED_KEY]
146             sign_used = key.content[KEY_SIGN_USED_KEY]
147
148         deferreds = []
149         for active in actives:
150             if KEY_VERSION_KEY in active.content:
151                 continue
152
153             active.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION
154             active.content[KEY_FINGERPRINT_KEY] = fingerprint
155             active.content[KEY_VALIDATION_KEY] = validation
156             active.content[KEY_LAST_AUDITED_AT_KEY] = last_audited
157             active.content[KEY_ENCR_USED_KEY] = encr_used
158             active.content[KEY_SIGN_USED_KEY] = sign_used
159             del active.content[KEY_ID_KEY]
160             d = self._soledad.put_doc(active)
161             deferreds.append(d)
162         return gatherResults(deferreds)
163
164     def _migrate_key(self, key):
165         if not key or KEY_VERSION_KEY in key.content:
166             return succeed(None)
167
168         key.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION
169         key.content[KEY_UIDS_KEY] = key.content[KEY_ADDRESS_KEY]
170         del key.content[KEY_ADDRESS_KEY]
171         del key.content[KEY_ID_KEY]
172         del key.content[KEY_VALIDATION_KEY]
173         del key.content[KEY_LAST_AUDITED_AT_KEY]
174         del key.content[KEY_ENCR_USED_KEY]
175         del key.content[KEY_SIGN_USED_KEY]
176         return self._soledad.put_doc(key)
177
178
179 def fp2id(fingerprint):
180     KEY_ID_LENGTH = 16
181     return fingerprint[-KEY_ID_LENGTH:]