[test] add updater tests
[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_VERSION_KEY,
36     KEY_FINGERPRINT_KEY,
37     KEY_VALIDATION_KEY,
38     KEY_LAST_AUDITED_AT_KEY,
39     KEY_ENCR_USED_KEY,
40     KEY_SIGN_USED_KEY,
41 )
42 from leap.keymanager.validation import ValidationLevels
43
44
45 KEY_ID_KEY = 'key_id'
46
47 KeyDocs = namedtuple("KeyDocs", ['key', 'active'])
48
49
50 class KeyDocumentsMigrator(object):
51     """
52     Migrate old KeyManager Soledad Documents to the newest schema
53     """
54
55     def __init__(self, soledad):
56         self._soledad = soledad
57
58     def migrate(self):
59         deferred_public = self._get_docs(private=False)
60         deferred_public.addCallback(self._migrate_docs)
61
62         deferred_private = self._get_docs(private=True)
63         deferred_private.addCallback(self._migrate_docs)
64
65         return gatherResults([deferred_public, deferred_private])
66
67     def _get_docs(self, private=False):
68         private_value = '1' if private else '0'
69
70         deferred_keys = self._soledad.get_from_index(
71             TAGS_PRIVATE_INDEX,
72             KEYMANAGER_KEY_TAG,
73             private_value)
74         deferred_active = self._soledad.get_from_index(
75             TAGS_PRIVATE_INDEX,
76             KEYMANAGER_ACTIVE_TAG,
77             private_value)
78         return gatherResults([deferred_keys, deferred_active])
79
80     def _migrate_docs(self, (key_docs, active_docs)):
81         def update_keys(keys):
82             deferreds = []
83             for key_id in keys:
84                 key = keys[key_id].key
85                 actives = keys[key_id].active
86
87                 d = self._migrate_actives(key, actives)
88                 deferreds.append(d)
89
90                 d = self._migrate_key(key)
91                 deferreds.append(d)
92             return gatherResults(deferreds)
93
94         d = self._buildKeyDict(key_docs, active_docs)
95         d.addCallback(lambda keydict: self._filter_outdated(keydict))
96         d.addCallback(update_keys)
97
98     def _buildKeyDict(self, keys, actives):
99         keydict = {
100             fp2id(key.content[KEY_FINGERPRINT_KEY]): KeyDocs(key, [])
101             for key in keys}
102
103         deferreds = []
104         for active in actives:
105             if KEY_ID_KEY in active.content:
106                 key_id = active.content[KEY_ID_KEY]
107                 if key_id not in keydict:
108                     d = self._soledad.delete_doc(active)
109                     deferreds.append(d)
110                     continue
111                 keydict[key_id].active.append(active)
112
113         d = gatherResults(deferreds)
114         d.addCallback(lambda _: keydict)
115         return d
116
117     def _filter_outdated(self, keydict):
118         outdated = {}
119         for key_id, docs in keydict.items():
120             if ((docs.key and KEY_VERSION_KEY not in docs.key.content) or
121                     docs.active):
122                 outdated[key_id] = docs
123         return outdated
124
125     def _migrate_actives(self, key, actives):
126         if not key:
127             deferreds = []
128             for active in actives:
129                 d = self._soledad.delete_doc(active)
130                 deferreds.append(d)
131             return gatherResults(deferreds)
132
133         validation = str(ValidationLevels.Weak_Chain)
134         last_audited = 0
135         encr_used = False
136         sign_used = False
137         fingerprint = key.content[KEY_FINGERPRINT_KEY]
138         if len(actives) == 1 and KEY_VERSION_KEY not in key.content:
139             # we can preserve the validation of the key if there is only one
140             # active address for the key
141             validation = key.content[KEY_VALIDATION_KEY]
142             last_audited = key.content[KEY_LAST_AUDITED_AT_KEY]
143             encr_used = key.content[KEY_ENCR_USED_KEY]
144             sign_used = key.content[KEY_SIGN_USED_KEY]
145
146         deferreds = []
147         for active in actives:
148             if KEY_VERSION_KEY in active.content:
149                 continue
150
151             active.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION
152             active.content[KEY_FINGERPRINT_KEY] = fingerprint
153             active.content[KEY_VALIDATION_KEY] = validation
154             active.content[KEY_LAST_AUDITED_AT_KEY] = last_audited
155             active.content[KEY_ENCR_USED_KEY] = encr_used
156             active.content[KEY_SIGN_USED_KEY] = sign_used
157             del active.content[KEY_ID_KEY]
158             d = self._soledad.put_doc(active)
159             deferreds.append(d)
160         return gatherResults(deferreds)
161
162     def _migrate_key(self, key):
163         if not key or KEY_VERSION_KEY in key.content:
164             return succeed(None)
165
166         key.content[KEY_VERSION_KEY] = KEYMANAGER_DOC_VERSION
167         del key.content[KEY_ID_KEY]
168         del key.content[KEY_VALIDATION_KEY]
169         del key.content[KEY_LAST_AUDITED_AT_KEY]
170         del key.content[KEY_ENCR_USED_KEY]
171         del key.content[KEY_SIGN_USED_KEY]
172         return self._soledad.put_doc(key)
173
174
175 def fp2id(fingerprint):
176     KEY_ID_LENGTH = 16
177     return fingerprint[-KEY_ID_LENGTH:]