Skip to main content

rite_model/params/
sharing.rs

1//! Params for the secret sharing actions.
2
3use schemars::{JsonSchema, Schema, SchemaGenerator};
4use serde::{Deserialize, Serialize};
5use serde_json::json;
6
7use super::schema::{add_rule, names, property};
8use crate::{RevealFormat, SharingScheme, bip39};
9
10/// Params for `reveal` action.
11#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
12#[serde(deny_unknown_fields)]
13#[schemars(transform = phrase_lengths)]
14#[non_exhaustive]
15pub struct RevealParams {
16    /// What the value is and what to do with it, shown above it and
17    /// printed on the sheet.
18    pub message: String,
19    /// The encoding: `paper32` (the default), `hex`, or `bip39` for a
20    /// wallet's recovery phrase.
21    #[serde(default)]
22    pub format: Option<RevealFormat>,
23    /// Text printed on the sheet: where it goes, who keeps it.
24    #[serde(default)]
25    pub note: Option<String>,
26    /// The value's length in the format's unit, words for `bip39` and
27    /// bytes otherwise, when the author knows it, so the sheet has exactly
28    /// the right number of rows; a value of another size is refused. A
29    /// share counts the three header bytes in front of its secret.
30    #[serde(default)]
31    #[schemars(range(min = 1))]
32    pub length: Option<u64>,
33}
34
35/// A `length:` in words holds a phrase length.
36fn phrase_lengths(schema: &mut Schema) {
37    let format = RevealFormat::Bip39;
38    add_rule(
39        schema,
40        json!({
41            "if": {
42                "properties": { "format": { "const": format.as_str() } },
43                "required": ["format"],
44            },
45            "then": {
46                "properties": {
47                    "length": {
48                        "enum": bip39::phrase_lengths().collect::<Vec<_>>(),
49                        "x-message": format!(
50                            "'length' counts words for '{format}', and a phrase has {} words",
51                            bip39::PHRASE_LENGTHS
52                        ),
53                    }
54                }
55            },
56        }),
57    );
58}
59
60/// Params for `enter_share` action.
61#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
62#[serde(deny_unknown_fields)]
63#[schemars(transform = longer_than_a_header)]
64#[non_exhaustive]
65pub struct EnterShareParams {
66    /// What the share is, shown above the rows: the sheet's heading.
67    pub message: String,
68    /// The encoding the sheet was written in: `paper32` (the default) or
69    /// `hex`.
70    #[serde(default)]
71    #[schemars(schema_with = "share_formats")]
72    pub format: Option<RevealFormat>,
73    /// Shown with the rows: where the sheet came from, who types it.
74    #[serde(default)]
75    pub note: Option<String>,
76    /// The share's length in bytes, its secret's plus three header bytes,
77    /// when the author knows it, so the prompt asks for exactly the right
78    /// rows; a share of another size is refused.
79    #[serde(default)]
80    pub length: Option<u64>,
81}
82
83/// A share is bytes, so it is never written in words. Naming a words format
84/// is told why; any other name gets the list.
85fn share_formats(_: &mut SchemaGenerator) -> Schema {
86    let (words, bytes): (Vec<RevealFormat>, Vec<RevealFormat>) = RevealFormat::ALL
87        .iter()
88        .partition(|format| format.is_words());
89    let mut schema = names(bytes.iter().map(|format| format.as_str()));
90    let words: Vec<&str> = words.iter().map(|format| format.as_str()).collect();
91    schema.insert(
92        "allOf".to_string(),
93        json!([{
94            "not": { "enum": words },
95            "x-message": format!(
96                "a share is written in {}; words are for the secret the shares combine into",
97                crate::types::either(bytes.iter().map(|format| format.as_str()))
98            ),
99        }]),
100    );
101    schema
102}
103
104/// A share is its secret plus a header, so it is longer than the header.
105fn longer_than_a_header(schema: &mut Schema) {
106    let header = SharingScheme::RiteSssV1.header_len();
107    // The message is the minimum's alone, beside the bounds the type sets.
108    property(schema, "length").insert(
109        "allOf".to_string(),
110        json!([{
111            "minimum": header.saturating_add(1),
112            "x-message": format!(
113                "'length' is a share's secret plus {header} header bytes, so it is longer \
114                 than {header}"
115            ),
116        }]),
117    );
118}
119
120/// Params for `split_secret` action.
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
122#[serde(deny_unknown_fields)]
123#[schemars(transform = share_limits)]
124#[non_exhaustive]
125pub struct SplitSecretParams {
126    /// The sharing scheme, defaulting to `rite-sss/v1`, the only one this
127    /// build implements. Named so the transcript says which one was used.
128    #[serde(default)]
129    pub scheme: Option<SharingScheme>,
130    /// How many shares reconstruct the secret, from 2 to the scheme's limit.
131    //
132    // One share is not a split.
133    #[schemars(range(min = 2))]
134    pub threshold: u8,
135    /// How many shares to make, from `threshold` to the scheme's limit, 100
136    /// for `rite-sss/v1`. Every subset of `threshold` shares is checked
137    /// before the step completes, so a split with more than 100,000 such
138    /// subsets is refused as well.
139    #[schemars(range(min = 2))]
140    pub shares: u8,
141}
142
143/// The default scheme when a step names none.
144const DEFAULT_SHARING_SCHEME: SharingScheme = SharingScheme::RiteSssV1;
145
146/// Each scheme's limit on the counts, for a step naming it; the default
147/// scheme's for a step naming none. Above the largest limit, no scheme takes
148/// the counts, which holds while `scheme` is an expression `rite check`
149/// cannot read.
150fn share_limits(schema: &mut Schema) {
151    let largest = SharingScheme::ALL
152        .iter()
153        .map(|scheme| scheme.max_shares())
154        .max()
155        .unwrap_or(u8::MAX);
156    add_rule(
157        schema,
158        json!({
159            "properties": {
160                "threshold": { "maximum": largest },
161                "shares": { "maximum": largest },
162            }
163        }),
164    );
165    for &scheme in SharingScheme::ALL {
166        let max = scheme.max_shares();
167        let names_it = if scheme == DEFAULT_SHARING_SCHEME {
168            json!({ "properties": { "scheme": { "const": scheme.as_str() } } })
169        } else {
170            json!({
171                "properties": { "scheme": { "const": scheme.as_str() } },
172                "required": ["scheme"],
173            })
174        };
175        add_rule(
176            schema,
177            json!({
178                "if": names_it,
179                "then": {
180                    "properties": {
181                        "threshold": { "maximum": max },
182                        "shares": { "maximum": max },
183                    }
184                },
185            }),
186        );
187    }
188}
189
190impl Default for SplitSecretParams {
191    fn default() -> Self {
192        Self {
193            scheme: None,
194            threshold: 2,
195            shares: 2,
196        }
197    }
198}