File: migrations.py

package info (click to toggle)
python-x3dh 1.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 356 kB
  • sloc: python: 1,259; makefile: 15
file content (158 lines) | stat: -rw-r--r-- 5,574 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
from __future__ import annotations

import base64
from typing import List, Tuple, cast

from pydantic import BaseModel

from .models import IdentityKeyPairModel, SignedPreKeyPairModel, BaseStateModel
from .types import JSONObject, SecretType


__all__ = [
    "parse_identity_key_pair_model",
    "parse_signed_pre_key_pair_model",
    "parse_base_state_model"
]


class PreStableKeyPairModel(BaseModel):
    """
    This model describes how a key pair was serialized in pre-stable serialization format.
    """

    priv: str
    pub: str


class PreStableSignedPreKeyModel(BaseModel):
    """
    This model describes how a signed pre-key was serialized in pre-stable serialization format.
    """

    key: PreStableKeyPairModel
    signature: str
    timestamp: float


class PreStableModel(BaseModel):
    """
    This model describes how State instances were serialized in pre-stable serialization format.
    """

    changed: bool
    ik: PreStableKeyPairModel
    spk: PreStableSignedPreKeyModel
    otpks: List[PreStableKeyPairModel]


def parse_identity_key_pair_model(serialized: JSONObject) -> IdentityKeyPairModel:
    """
    Parse a serialized :class:`~x3dh.identity_key_pair.IdentityKeyPair` instance, as returned by
    :attr:`~x3dh.identity_key_pair.IdentityKeyPair.json`, into the most recent pydantic model available for
    the class. Perform migrations in case the pydantic models were updated.

    Args:
        serialized: The serialized instance.

    Returns:
        The model, which can be used to restore the instance using
        :meth:`~x3dh.identity_key_pair.IdentityKeyPair.from_model`.

    Note:
        Pre-stable data can only be migrated as a whole using :func:`parse_base_state_model`.
    """

    # Each model has a Python string "version" in its root. Use that to find the model that the data was
    # serialized from.
    version = cast(str, serialized["version"])
    model: BaseModel = {
        "1.0.0": IdentityKeyPairModel,
        "1.0.1": IdentityKeyPairModel
    }[version](**serialized)  # type: ignore[arg-type]

    # Once all migrations have been applied, the model should be an instance of the most recent model
    assert isinstance(model, IdentityKeyPairModel)

    return model


def parse_signed_pre_key_pair_model(serialized: JSONObject) -> SignedPreKeyPairModel:
    """
    Parse a serialized :class:`~x3dh.signed_pre_key_pair.SignedPreKeyPair` instance, as returned by
    :attr:`~x3dh.signed_pre_key_pair.SignedPreKeyPair.json`, into the most recent pydantic model available for
    the class. Perform migrations in case the pydantic models were updated.

    Args:
        serialized: The serialized instance.

    Returns:
        The model, which can be used to restore the instance using
        :meth:`~x3dh.signed_pre_key_pair.SignedPreKeyPair.from_model`.

    Note:
        Pre-stable data can only be migrated as a whole using :func:`parse_base_state_model`.
    """

    # Each model has a Python string "version" in its root. Use that to find the model that the data was
    # serialized from.
    version = cast(str, serialized["version"])
    model: BaseModel = {
        "1.0.0": SignedPreKeyPairModel,
        "1.0.1": SignedPreKeyPairModel
    }[version](**serialized)  # type: ignore[arg-type]

    # Once all migrations have been applied, the model should be an instance of the most recent model
    assert isinstance(model, SignedPreKeyPairModel)

    return model


def parse_base_state_model(serialized: JSONObject) -> Tuple[BaseStateModel, bool]:
    """
    Parse a serialized :class:`~x3dh.base_state.BaseState` instance, as returned by
    :attr:`~x3dh.base_state.BaseState.json`, into the most recent pydantic model available for the class.
    Perform migrations in case the pydantic models were updated. Supports migration of pre-stable data.

    Args:
        serialized: The serialized instance.

    Returns:
        The model, which can be used to restore the instance using
        :meth:`~x3dh.base_state.BaseState.from_model`, and a flag that indicates whether the bundle needs to
        be published, which was part of the pre-stable serialization format.
    """

    bundle_needs_publish = False

    # Each model has a Python string "version" in its root. Use that to find the model that the data was
    # serialized from. Special case: the pre-stable serialization format does not contain a version.
    version = cast(str, serialized["version"]) if "version" in serialized else None
    model: BaseModel = {
        None: PreStableModel,
        "1.0.0": BaseStateModel,
        "1.0.1": BaseStateModel
    }[version](**serialized)

    if isinstance(model, PreStableModel):
        # Run migrations from PreStableModel to StateModel
        bundle_needs_publish = bundle_needs_publish or model.changed

        model = BaseStateModel(
            identity_key=IdentityKeyPairModel(
                secret=base64.b64decode(model.ik.priv),
                secret_type=SecretType.PRIV
            ),
            signed_pre_key=SignedPreKeyPairModel(
                priv=base64.b64decode(model.spk.key.priv),
                sig=base64.b64decode(model.spk.signature),
                timestamp=int(model.spk.timestamp)
            ),
            old_signed_pre_key=None,
            pre_keys=frozenset({ base64.b64decode(pre_key.priv) for pre_key in model.otpks })
        )

    # Once all migrations have been applied, the model should be an instance of the most recent model
    assert isinstance(model, BaseStateModel)

    return model, bundle_needs_publish