Files: 6df8f9822940a85660caf16ce3b5f32f30589248 / lib / depject / page / html / render / mnemonic.js
6887 bytesRaw
1 | const fs = require("fs"); |
2 | const fsPath = require("path"); |
3 | const { computed, h, Value } = require("mutant"); |
4 | const nest = require("depnest"); |
5 | const ssbMnemonic = require("ssb-keys-mnemonic"); |
6 | const watch = require("mutant/watch"); |
7 | |
8 | exports.needs = nest({ |
9 | "message.html.markdown": "first", |
10 | "intl.sync.i18n": "first", |
11 | "keys.sync.load": "first", |
12 | }); |
13 | |
14 | exports.gives = nest("page.html.render"); |
15 | |
16 | exports.create = function (api) { |
17 | return nest("page.html.render", function channel(path) { |
18 | const assetPath = fsPath.join( |
19 | __dirname, |
20 | "..", |
21 | "..", |
22 | "..", |
23 | "..", |
24 | "..", |
25 | "assets", |
26 | "mnemonic_warning.md" |
27 | ); |
28 | const markdown = api.message.html.markdown |
29 | const warningText = fs.readFileSync(assetPath, "utf8"); |
30 | const warningHtml = markdown(warningText); |
31 | |
32 | const confirmationText = Value(""); |
33 | |
34 | if (path !== "/mnemonic") return; |
35 | const i18n = api.intl.sync.i18n; |
36 | |
37 | const keys = api.keys.sync.load(); |
38 | const words = ssbMnemonic.keysToWords(keys).split(" "); |
39 | const wordBatches = []; |
40 | const maxLen = words.reduce( |
41 | (currMax, currWord) => |
42 | currWord.length > currMax ? currWord.length : currMax, |
43 | 0 |
44 | ); |
45 | for (let i = 0; i < words.length; i = i + 4) { |
46 | const batchLine = words |
47 | .slice(i, i + 4) |
48 | .map((s) => s.padEnd(maxLen, " ")) |
49 | .join(" "); |
50 | wordBatches.push(batchLine); |
51 | } |
52 | const mnemonic = wordBatches.join("\n"); |
53 | |
54 | const prepend = [ |
55 | h("PageHeading", [h("h1", [h("strong", i18n("Key Export"))])]), |
56 | ]; |
57 | |
58 | const content = [h("section", warningHtml), h("hr")]; |
59 | |
60 | let showNextChallenge = Value(false) |
61 | const showFirstChallenge = showNextChallenge |
62 | content.push( |
63 | h( |
64 | "form", |
65 | { |
66 | style: { |
67 | margin: "1em auto", |
68 | }, |
69 | action: "", |
70 | "ev-submit": (ev) => { |
71 | ev.preventDefault(); |
72 | showFirstChallenge.set(true); |
73 | }, |
74 | }, |
75 | h( |
76 | "button", |
77 | { |
78 | disabled: showFirstChallenge, |
79 | style: { |
80 | margin: "1em auto", |
81 | "background-color": "#51c067", |
82 | color: "white", |
83 | }, |
84 | }, |
85 | "I still want to export my keys" |
86 | ), |
87 | ) |
88 | ); |
89 | |
90 | function addChallenge(challenge, response) { |
91 | const showChalllenge = showNextChallenge; |
92 | const showNext = Value(false); |
93 | content.push( |
94 | h( |
95 | "section", |
96 | { hidden: computed(showChalllenge, (b) => !b) }, |
97 | challenge |
98 | ), |
99 | h( |
100 | "form", |
101 | { |
102 | hidden: computed(showChalllenge, (b) => !b), |
103 | style: { |
104 | margin: "1em auto", |
105 | }, |
106 | action: "", |
107 | "ev-submit": (ev) => { |
108 | ev.preventDefault(); |
109 | if (confirmationText().toLowerCase() === response.toLowerCase()) { |
110 | showNext.set(true); |
111 | } |
112 | }, |
113 | }, |
114 | [ |
115 | h("input", { |
116 | disabled: showNext, |
117 | hooks: [ValueHook(confirmationText), ScrollHook(showChalllenge)], |
118 | size: response.length + 1 < 30 ? 30 : response.length + 1, |
119 | "ev-paste": (ev) => { |
120 | ev.preventDefault(); |
121 | }, |
122 | }), |
123 | ] |
124 | ) |
125 | ); |
126 | showNextChallenge = showNext |
127 | } |
128 | |
129 | addChallenge( |
130 | [ |
131 | markdown('> ' + i18n("To confirm you understand the risks and responsibilities here, we will play a little game. Are you ready?")), |
132 | i18n("Type 'yes' and it return to continue"), |
133 | ], |
134 | i18n('yes'), |
135 | ); |
136 | addChallenge( |
137 | markdown('> ' + i18n("This will be annoying and slow. That's intentional. It's a feature, not a bug. You really need to understand that we can't help you if this goes wrong. Type 'I understand' to confirm")), |
138 | i18n('I understand'), |
139 | ); |
140 | addChallenge( |
141 | markdown('> ' + i18n("Good. You'll answer a bunch of questions. The prize for getting them right is one giant foot-gun. Aka: your key export\nLet's start easy: What's the name of the person posting the bird picture?")), |
142 | i18n('Carol'), |
143 | ); |
144 | addChallenge( |
145 | markdown('> ' + i18n("Excellent job. And that's the same person as the one posting about #foffee, right?")), |
146 | i18n('no'), |
147 | ); |
148 | addChallenge( |
149 | markdown('> ' + i18n("Well *someone* has been paying attention. Good catch! So, who was it then?")), |
150 | i18n('Alice'), |
151 | ); |
152 | addChallenge( |
153 | markdown('> ' + i18n("You made it quite far into the text, that's good news!\nChange of gears: Will this procedure allow you to use the same identity on multiple two or more devices?")), |
154 | i18n('no'), |
155 | ); |
156 | const sameAsChallenge = i18n('I understand that exporting my key will not allow me to use it on more than one device.') |
157 | addChallenge( |
158 | markdown('> ' + i18n("That's right. But let's be clear here. Please type this out:")+'\n> '+sameAsChallenge), |
159 | sameAsChallenge, |
160 | ); |
161 | addChallenge( |
162 | markdown('> ' + i18n("At which step of the process does manyverse become the sole holder of your identity?")), |
163 | i18n('9'), |
164 | ); |
165 | addChallenge( |
166 | markdown('> ' + i18n("Correct. Just after you import the key.\nIs it safe to post at that point then?")), |
167 | i18n('no'), |
168 | ); |
169 | addChallenge( |
170 | markdown('> ' + i18n("Okay, looks like you're getting it. One last one then:\nIf any of this goes wrong, who can most likely help you?\n* The patchwork devs\n* The manyverse devs\n* Nobody")), |
171 | i18n('nobody'), |
172 | ); |
173 | |
174 | content.push( |
175 | h("hr", { hidden: computed(showNextChallenge, (b) => !b) }), |
176 | h("div", { hidden: computed(showNextChallenge, (b) => !b) }, [ |
177 | h("p", i18n("Congrats, you made it. Here your mnemonic representation of your secret:")), |
178 | h("pre.mnemonic", { |
179 | hooks: [ScrollHook(showNextChallenge)], |
180 | }, mnemonic), |
181 | h( |
182 | "p", |
183 | i18n( |
184 | "Again: be very careful with it. Keep it secret, and don't use this key on multiple devices, including this one." |
185 | ) |
186 | ), |
187 | ]) |
188 | ); |
189 | |
190 | return h("Scroller", { style: { overflow: "auto" } }, [ |
191 | h("div.wrapper", [ |
192 | h( |
193 | "section.prepend", |
194 | h("PageHeading", [h("h1", [h("strong", i18n("Key Export"))])]) |
195 | ), |
196 | h("section.content", content), |
197 | ]), |
198 | ]); |
199 | }); |
200 | }; |
201 | |
202 | function ValueHook(obs) { |
203 | return function (element) { |
204 | element.value = obs(); |
205 | element.oninput = function () { |
206 | obs.set(element.value.trim()); |
207 | }; |
208 | }; |
209 | } |
210 | |
211 | function ScrollHook(obs) { |
212 | return function(element) { |
213 | var scrolledOnce = false |
214 | watch(obs, (visible) => { |
215 | if (!scrolledOnce && visible) { |
216 | scrolledOnce = true |
217 | element.scrollIntoViewIfNeeded() |
218 | try { |
219 | element.focus() |
220 | } catch {} |
221 | } |
222 | }) |
223 | } |
224 | } |
225 |
Built with git-ssb-web