@webwriter/quiz 1.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +778 -0
- package/assets/icons/grip-horizontal.svg +8 -0
- package/assets/icons/grip-vertical.svg +8 -0
- package/assets/icons/truefalse.svg +6 -0
- package/custom-elements.json +2779 -0
- package/custom.d.ts +5 -1
- package/dist/api/index.d.ts +27 -0
- package/dist/api/index.js +40 -0
- package/dist/migrate.js +1434 -0
- package/dist/widgets/webwriter-choice-item.js +4095 -3818
- package/dist/widgets/webwriter-choice.js +2179 -5598
- package/dist/widgets/webwriter-gap-item.js +3987 -0
- package/dist/widgets/webwriter-gap.js +2908 -0
- package/dist/widgets/webwriter-mark.js +4347 -3368
- package/dist/widgets/webwriter-order-item.js +4038 -3197
- package/dist/widgets/webwriter-order.js +2885 -4200
- package/dist/widgets/webwriter-pairing-item.js +1070 -2330
- package/dist/widgets/webwriter-pairing.js +2968 -4231
- package/dist/widgets/webwriter-quiz-hint.js +2469 -0
- package/dist/widgets/webwriter-quiz.js +3337 -4364
- package/dist/widgets/webwriter-speech.js +6100 -5120
- package/dist/widgets/webwriter-task-prompt.js +4787 -2068
- package/dist/widgets/webwriter-task.js +2264 -8157
- package/dist/widgets/webwriter-text.js +4075 -4132
- package/dist/widgets/webwriter-true-false.js +4674 -0
- package/editing-config.json +106 -0
- package/lit-localize.json +20 -14
- package/localization/de.xlf +395 -65
- package/localization/es.xlf +396 -61
- package/localization/fr.xlf +396 -61
- package/localization/generated/de.ts +97 -15
- package/localization/generated/es.ts +97 -15
- package/localization/generated/fr.ts +98 -16
- package/localization/generated/index.d.ts +5 -0
- package/localization/generated/it.ts +96 -14
- package/localization/generated/locale-codes.js +0 -48
- package/localization/generated/nl.ts +99 -17
- package/localization/it.xlf +396 -61
- package/localization/nl.xlf +396 -61
- package/package.json +120 -254
- package/scripts/apply-translations.mjs +97 -0
- package/src/api/index.ts +72 -0
- package/src/lib/artboard.ts +686 -0
- package/src/lib/draggable-grid.ts +432 -0
- package/src/lib/encrypted-property.ts +202 -0
- package/src/lib/hint-popup.ts +152 -0
- package/src/lib/utils.ts +97 -0
- package/src/lib/webwriter-quirks.ts +121 -0
- package/src/migrate/index.ts +622 -0
- package/src/migrate/legacy-solution.ts +362 -0
- package/src/snippets/order.html +8 -7
- package/src/widgets/choice/webwriter-choice-item.ts +316 -0
- package/src/widgets/choice/webwriter-choice.ts +328 -0
- package/src/widgets/choice/webwriter-true-false.ts +113 -0
- package/src/widgets/dnd/webwriter-dnd-draggable.ts +28 -0
- package/src/widgets/dnd/webwriter-dnd-dropzone.ts +9 -0
- package/src/widgets/dnd/webwriter-dnd.ts +36 -0
- package/src/widgets/gap/webwriter-gap-item.ts +250 -0
- package/src/widgets/gap/webwriter-gap.ts +447 -0
- package/src/widgets/order/webwriter-order-item.ts +243 -0
- package/src/widgets/order/webwriter-order.ts +282 -0
- package/src/widgets/pairing/webwriter-pairing-item.ts +46 -0
- package/src/widgets/pairing/webwriter-pairing.ts +934 -0
- package/src/widgets/speech/audio-player.ts +325 -0
- package/src/widgets/speech/audio-recorder.ts +285 -0
- package/src/widgets/speech/waveform.ts +113 -0
- package/src/widgets/speech/webwriter-speech.ts +87 -0
- package/src/widgets/webwriter-mark.ts +366 -424
- package/src/widgets/webwriter-quiz-hint.ts +36 -0
- package/src/widgets/webwriter-quiz.ts +318 -280
- package/src/widgets/webwriter-task-prompt.ts +43 -18
- package/src/widgets/webwriter-task.ts +212 -551
- package/src/widgets/webwriter-text.ts +286 -172
- package/tsconfig.api.json +12 -0
- package/tsconfig.json +7 -6
- package/dist/widgets/webwriter-choice.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.css +0 -444
- package/dist/widgets/webwriter-cloze-gap.js +0 -5727
- package/dist/widgets/webwriter-cloze.css +0 -444
- package/dist/widgets/webwriter-cloze.js +0 -3653
- package/dist/widgets/webwriter-mark.css +0 -444
- package/dist/widgets/webwriter-order.css +0 -444
- package/dist/widgets/webwriter-pairing-item.css +0 -444
- package/dist/widgets/webwriter-pairing.css +0 -444
- package/dist/widgets/webwriter-quiz.css +0 -444
- package/dist/widgets/webwriter-task-explainer.css +0 -444
- package/dist/widgets/webwriter-task-explainer.js +0 -2769
- package/dist/widgets/webwriter-task-hint.css +0 -444
- package/dist/widgets/webwriter-task-hint.js +0 -2708
- package/dist/widgets/webwriter-task.css +0 -444
- package/dist/widgets/webwriter-text.css +0 -444
- package/localization/bg.xlf +0 -72
- package/localization/cs.xlf +0 -72
- package/localization/da.xlf +0 -72
- package/localization/el.xlf +0 -72
- package/localization/et.xlf +0 -72
- package/localization/fi.xlf +0 -72
- package/localization/generated/bg.ts +0 -31
- package/localization/generated/cs.ts +0 -31
- package/localization/generated/da.ts +0 -31
- package/localization/generated/el.ts +0 -31
- package/localization/generated/et.ts +0 -31
- package/localization/generated/fi.ts +0 -31
- package/localization/generated/hu.ts +0 -31
- package/localization/generated/id.ts +0 -31
- package/localization/generated/ja.ts +0 -31
- package/localization/generated/ko.ts +0 -31
- package/localization/generated/lt.ts +0 -31
- package/localization/generated/lv.ts +0 -31
- package/localization/generated/nb.ts +0 -31
- package/localization/generated/pl.ts +0 -31
- package/localization/generated/pt-BR.ts +0 -31
- package/localization/generated/pt-PT.ts +0 -31
- package/localization/generated/ro.ts +0 -31
- package/localization/generated/ru.ts +0 -31
- package/localization/generated/sk.ts +0 -31
- package/localization/generated/sl.ts +0 -31
- package/localization/generated/sv.ts +0 -31
- package/localization/generated/tr.ts +0 -31
- package/localization/generated/uk.ts +0 -31
- package/localization/generated/zh-hans.ts +0 -31
- package/localization/hu.xlf +0 -72
- package/localization/id.xlf +0 -72
- package/localization/ja.xlf +0 -72
- package/localization/ko.xlf +0 -72
- package/localization/lt.xlf +0 -72
- package/localization/lv.xlf +0 -72
- package/localization/nb.xlf +0 -72
- package/localization/pl.xlf +0 -72
- package/localization/pt-BR.xlf +0 -72
- package/localization/pt-PT.xlf +0 -72
- package/localization/ro.xlf +0 -72
- package/localization/ru.xlf +0 -72
- package/localization/sk.xlf +0 -72
- package/localization/sl.xlf +0 -72
- package/localization/sv.xlf +0 -72
- package/localization/tr.xlf +0 -72
- package/localization/uk.xlf +0 -72
- package/localization/zh-hans.xlf +0 -72
- package/src/lib/combobox.ts +0 -456
- package/src/lib/highlighter-fill.svg +0 -6
- package/src/snippets/Beispiel-Choice.html +0 -57
- package/src/snippets/Beispiel-Mark.html +0 -14
- package/src/snippets/Beispiel-Order.html +0 -52
- package/src/snippets/Beispiel-Text.html +0 -20
- package/src/snippets/blank.html +0 -1
- package/src/snippets/choice.html +0 -8
- package/src/snippets/cloze.html +0 -4
- package/src/snippets/mark.html +0 -4
- package/src/snippets/pairing.html +0 -10
- package/src/snippets/speech.html +0 -4
- package/src/snippets/text.html +0 -4
- package/src/snippets/wordsearch.html +0 -4
- package/src/widgets/webwriter-choice-item.ts +0 -257
- package/src/widgets/webwriter-choice.ts +0 -353
- package/src/widgets/webwriter-cloze-gap.ts +0 -216
- package/src/widgets/webwriter-cloze.ts +0 -136
- package/src/widgets/webwriter-order-item.ts +0 -441
- package/src/widgets/webwriter-order.ts +0 -327
- package/src/widgets/webwriter-pairing-item.ts +0 -161
- package/src/widgets/webwriter-pairing.ts +0 -193
- package/src/widgets/webwriter-speech.ts +0 -273
- package/src/widgets/webwriter-task-explainer.ts +0 -43
- package/src/widgets/webwriter-task-hint.ts +0 -23
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous reimplementation of the solution encryption used by
|
|
3
|
+
* `@webwriter/quiz` v1, which stored every answer's solution on
|
|
4
|
+
* `<webwriter-task>` as `solution`/`salt`/`iv` data URLs. The payload was
|
|
5
|
+
* PBKDF2-SHA-256 (100000 iterations) derived from a hardcoded password and
|
|
6
|
+
* encrypted with AES-256-GCM.
|
|
7
|
+
*
|
|
8
|
+
* WebWriter runs migration scripts in an iframe and snapshots the document as
|
|
9
|
+
* soon as it loads, so the `migrate` handler cannot await anything. That rules
|
|
10
|
+
* out `crypto.subtle`, hence the hand-rolled primitives below. Only the
|
|
11
|
+
* AES-CTR half of GCM is implemented: the authentication tag is discarded
|
|
12
|
+
* rather than verified, which is fine because a wrong plaintext simply fails to
|
|
13
|
+
* parse as JSON.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The password v1 hardcoded in `webwriter-task.ts`. */
|
|
17
|
+
const LEGACY_PASSWORD = "B08bxd82SAOf";
|
|
18
|
+
const PBKDF2_ITERATIONS = 100000;
|
|
19
|
+
const GCM_TAG_LENGTH = 16;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// SHA-256
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const K = new Uint32Array([
|
|
26
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
|
|
27
|
+
0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
28
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
|
|
29
|
+
0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
30
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
|
|
31
|
+
0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
32
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
|
|
33
|
+
0xc67178f2,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const INITIAL_STATE = new Uint32Array([
|
|
37
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const schedule = new Uint32Array(64);
|
|
41
|
+
|
|
42
|
+
function compress(state: Uint32Array, block: Uint8Array, offset: number) {
|
|
43
|
+
for (let i = 0; i < 16; i++) {
|
|
44
|
+
const j = offset + i * 4;
|
|
45
|
+
schedule[i] = (block[j] << 24) | (block[j + 1] << 16) | (block[j + 2] << 8) | block[j + 3];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (let i = 16; i < 64; i++) {
|
|
49
|
+
const x = schedule[i - 15];
|
|
50
|
+
const y = schedule[i - 2];
|
|
51
|
+
const s0 = ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
|
|
52
|
+
const s1 = ((y >>> 17) | (y << 15)) ^ ((y >>> 19) | (y << 13)) ^ (y >>> 10);
|
|
53
|
+
schedule[i] = (schedule[i - 16] + s0 + schedule[i - 7] + s1) | 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let a = state[0];
|
|
57
|
+
let b = state[1];
|
|
58
|
+
let c = state[2];
|
|
59
|
+
let d = state[3];
|
|
60
|
+
let e = state[4];
|
|
61
|
+
let f = state[5];
|
|
62
|
+
let g = state[6];
|
|
63
|
+
let h = state[7];
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < 64; i++) {
|
|
66
|
+
const s1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
67
|
+
const ch = (e & f) ^ (~e & g);
|
|
68
|
+
const t1 = (h + s1 + ch + K[i] + schedule[i]) | 0;
|
|
69
|
+
const s0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
70
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
71
|
+
const t2 = (s0 + maj) | 0;
|
|
72
|
+
|
|
73
|
+
h = g;
|
|
74
|
+
g = f;
|
|
75
|
+
f = e;
|
|
76
|
+
e = (d + t1) | 0;
|
|
77
|
+
d = c;
|
|
78
|
+
c = b;
|
|
79
|
+
b = a;
|
|
80
|
+
a = (t1 + t2) | 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
state[0] = (state[0] + a) | 0;
|
|
84
|
+
state[1] = (state[1] + b) | 0;
|
|
85
|
+
state[2] = (state[2] + c) | 0;
|
|
86
|
+
state[3] = (state[3] + d) | 0;
|
|
87
|
+
state[4] = (state[4] + e) | 0;
|
|
88
|
+
state[5] = (state[5] + f) | 0;
|
|
89
|
+
state[6] = (state[6] + g) | 0;
|
|
90
|
+
state[7] = (state[7] + h) | 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const tailBlock = new Uint8Array(128);
|
|
94
|
+
const tailView = new DataView(tailBlock.buffer);
|
|
95
|
+
|
|
96
|
+
/** Feeds `data` into `state`, finishing the hash of `prefixLength` earlier bytes plus `data`. */
|
|
97
|
+
function absorb(state: Uint32Array, data: Uint8Array, prefixLength: number) {
|
|
98
|
+
let i = 0;
|
|
99
|
+
for (; i + 64 <= data.length; i += 64) compress(state, data, i);
|
|
100
|
+
|
|
101
|
+
const rest = data.length - i;
|
|
102
|
+
const size = rest <= 55 ? 64 : 128;
|
|
103
|
+
tailBlock.fill(0, 0, size);
|
|
104
|
+
tailBlock.set(data.subarray(i), 0);
|
|
105
|
+
tailBlock[rest] = 0x80;
|
|
106
|
+
|
|
107
|
+
const bits = (prefixLength + data.length) * 8;
|
|
108
|
+
tailView.setUint32(size - 8, Math.floor(bits / 0x100000000));
|
|
109
|
+
tailView.setUint32(size - 4, bits >>> 0);
|
|
110
|
+
|
|
111
|
+
for (let j = 0; j < size; j += 64) compress(state, tailBlock, j);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function writeState(state: Uint32Array, out: Uint8Array) {
|
|
115
|
+
for (let i = 0; i < 8; i++) {
|
|
116
|
+
out[i * 4] = state[i] >>> 24;
|
|
117
|
+
out[i * 4 + 1] = (state[i] >>> 16) & 0xff;
|
|
118
|
+
out[i * 4 + 2] = (state[i] >>> 8) & 0xff;
|
|
119
|
+
out[i * 4 + 3] = state[i] & 0xff;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sha256(data: Uint8Array) {
|
|
124
|
+
const state = INITIAL_STATE.slice();
|
|
125
|
+
absorb(state, data, 0);
|
|
126
|
+
const out = new Uint8Array(32);
|
|
127
|
+
writeState(state, out);
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// HMAC-SHA-256 and PBKDF2
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
type HmacStates = { inner: Uint32Array; outer: Uint32Array };
|
|
136
|
+
|
|
137
|
+
/** Precomputes the two padded-key states so each HMAC costs two compressions. */
|
|
138
|
+
function hmacStates(key: Uint8Array): HmacStates {
|
|
139
|
+
const normalized = key.length > 64 ? sha256(key) : key;
|
|
140
|
+
const ipad = new Uint8Array(64);
|
|
141
|
+
const opad = new Uint8Array(64);
|
|
142
|
+
ipad.set(normalized);
|
|
143
|
+
opad.set(normalized);
|
|
144
|
+
|
|
145
|
+
for (let i = 0; i < 64; i++) {
|
|
146
|
+
ipad[i] ^= 0x36;
|
|
147
|
+
opad[i] ^= 0x5c;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const inner = INITIAL_STATE.slice();
|
|
151
|
+
compress(inner, ipad, 0);
|
|
152
|
+
const outer = INITIAL_STATE.slice();
|
|
153
|
+
compress(outer, opad, 0);
|
|
154
|
+
return { inner, outer };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const hmacDigest = new Uint8Array(32);
|
|
158
|
+
|
|
159
|
+
function hmacInto(states: HmacStates, message: Uint8Array, out: Uint8Array) {
|
|
160
|
+
const inner = states.inner.slice();
|
|
161
|
+
absorb(inner, message, 64);
|
|
162
|
+
writeState(inner, hmacDigest);
|
|
163
|
+
|
|
164
|
+
const outer = states.outer.slice();
|
|
165
|
+
absorb(outer, hmacDigest, 64);
|
|
166
|
+
writeState(outer, out);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pbkdf2Sha256(password: Uint8Array, salt: Uint8Array, iterations: number, keyLength: number) {
|
|
170
|
+
const states = hmacStates(password);
|
|
171
|
+
const derived = new Uint8Array(keyLength);
|
|
172
|
+
const input = new Uint8Array(salt.length + 4);
|
|
173
|
+
const inputView = new DataView(input.buffer);
|
|
174
|
+
const u = new Uint8Array(32);
|
|
175
|
+
const t = new Uint8Array(32);
|
|
176
|
+
input.set(salt);
|
|
177
|
+
|
|
178
|
+
for (let block = 1; block <= Math.ceil(keyLength / 32); block++) {
|
|
179
|
+
inputView.setUint32(salt.length, block);
|
|
180
|
+
hmacInto(states, input, u);
|
|
181
|
+
t.set(u);
|
|
182
|
+
|
|
183
|
+
for (let i = 1; i < iterations; i++) {
|
|
184
|
+
hmacInto(states, u, u);
|
|
185
|
+
for (let j = 0; j < 32; j++) t[j] ^= u[j];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
derived.set(t.subarray(0, Math.min(32, keyLength - (block - 1) * 32)), (block - 1) * 32);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return derived;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// AES-256 in counter mode
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
const SBOX = new Uint8Array(256);
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
const rotl8 = (x: number, shift: number) => ((x << shift) | (x >>> (8 - shift))) & 0xff;
|
|
202
|
+
let p = 1;
|
|
203
|
+
let q = 1;
|
|
204
|
+
|
|
205
|
+
do {
|
|
206
|
+
p = (p ^ (p << 1) ^ (p & 0x80 ? 0x1b : 0)) & 0xff;
|
|
207
|
+
q ^= q << 1;
|
|
208
|
+
q ^= q << 2;
|
|
209
|
+
q ^= q << 4;
|
|
210
|
+
q &= 0xff;
|
|
211
|
+
if (q & 0x80) q ^= 0x09;
|
|
212
|
+
SBOX[p] = (q ^ rotl8(q, 1) ^ rotl8(q, 2) ^ rotl8(q, 3) ^ rotl8(q, 4) ^ 0x63) & 0xff;
|
|
213
|
+
} while (p !== 1);
|
|
214
|
+
|
|
215
|
+
SBOX[0] = 0x63;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const xtime = (x: number) => ((x << 1) ^ (x & 0x80 ? 0x1b : 0)) & 0xff;
|
|
219
|
+
|
|
220
|
+
const AES_ROUNDS = 14;
|
|
221
|
+
const AES_KEY_WORDS = 8;
|
|
222
|
+
|
|
223
|
+
function expandKey(key: Uint8Array) {
|
|
224
|
+
const roundKeys = new Uint8Array(16 * (AES_ROUNDS + 1));
|
|
225
|
+
roundKeys.set(key);
|
|
226
|
+
let rcon = 1;
|
|
227
|
+
|
|
228
|
+
for (let i = AES_KEY_WORDS; i < 4 * (AES_ROUNDS + 1); i++) {
|
|
229
|
+
const previous = (i - 1) * 4;
|
|
230
|
+
let t0 = roundKeys[previous];
|
|
231
|
+
let t1 = roundKeys[previous + 1];
|
|
232
|
+
let t2 = roundKeys[previous + 2];
|
|
233
|
+
let t3 = roundKeys[previous + 3];
|
|
234
|
+
|
|
235
|
+
if (i % AES_KEY_WORDS === 0) {
|
|
236
|
+
const rotated = t0;
|
|
237
|
+
t0 = SBOX[t1] ^ rcon;
|
|
238
|
+
t1 = SBOX[t2];
|
|
239
|
+
t2 = SBOX[t3];
|
|
240
|
+
t3 = SBOX[rotated];
|
|
241
|
+
rcon = xtime(rcon);
|
|
242
|
+
} else if (i % AES_KEY_WORDS === 4) {
|
|
243
|
+
t0 = SBOX[t0];
|
|
244
|
+
t1 = SBOX[t1];
|
|
245
|
+
t2 = SBOX[t2];
|
|
246
|
+
t3 = SBOX[t3];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const target = i * 4;
|
|
250
|
+
const source = (i - AES_KEY_WORDS) * 4;
|
|
251
|
+
roundKeys[target] = roundKeys[source] ^ t0;
|
|
252
|
+
roundKeys[target + 1] = roundKeys[source + 1] ^ t1;
|
|
253
|
+
roundKeys[target + 2] = roundKeys[source + 2] ^ t2;
|
|
254
|
+
roundKeys[target + 3] = roundKeys[source + 3] ^ t3;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return roundKeys;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function encryptBlock(roundKeys: Uint8Array, block: Uint8Array) {
|
|
261
|
+
for (let i = 0; i < 16; i++) block[i] ^= roundKeys[i];
|
|
262
|
+
|
|
263
|
+
for (let round = 1; round <= AES_ROUNDS; round++) {
|
|
264
|
+
for (let i = 0; i < 16; i++) block[i] = SBOX[block[i]];
|
|
265
|
+
|
|
266
|
+
let swap = block[1];
|
|
267
|
+
block[1] = block[5];
|
|
268
|
+
block[5] = block[9];
|
|
269
|
+
block[9] = block[13];
|
|
270
|
+
block[13] = swap;
|
|
271
|
+
|
|
272
|
+
swap = block[2];
|
|
273
|
+
const swap2 = block[6];
|
|
274
|
+
block[2] = block[10];
|
|
275
|
+
block[6] = block[14];
|
|
276
|
+
block[10] = swap;
|
|
277
|
+
block[14] = swap2;
|
|
278
|
+
|
|
279
|
+
swap = block[15];
|
|
280
|
+
block[15] = block[11];
|
|
281
|
+
block[11] = block[7];
|
|
282
|
+
block[7] = block[3];
|
|
283
|
+
block[3] = swap;
|
|
284
|
+
|
|
285
|
+
if (round < AES_ROUNDS) {
|
|
286
|
+
for (let c = 0; c < 16; c += 4) {
|
|
287
|
+
const a0 = block[c];
|
|
288
|
+
const a1 = block[c + 1];
|
|
289
|
+
const a2 = block[c + 2];
|
|
290
|
+
const a3 = block[c + 3];
|
|
291
|
+
const all = a0 ^ a1 ^ a2 ^ a3;
|
|
292
|
+
block[c] = a0 ^ all ^ xtime(a0 ^ a1);
|
|
293
|
+
block[c + 1] = a1 ^ all ^ xtime(a1 ^ a2);
|
|
294
|
+
block[c + 2] = a2 ^ all ^ xtime(a2 ^ a3);
|
|
295
|
+
block[c + 3] = a3 ^ all ^ xtime(a3 ^ a0);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const offset = round * 16;
|
|
300
|
+
for (let i = 0; i < 16; i++) block[i] ^= roundKeys[offset + i];
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** AES-256-GCM decryption without tag verification (GCM is CTR plus a MAC we ignore). */
|
|
305
|
+
function aesGcmDecrypt(key: Uint8Array, iv: Uint8Array, payload: Uint8Array) {
|
|
306
|
+
if (iv.length !== 12) throw new Error(`Unsupported IV length: ${iv.length}`);
|
|
307
|
+
if (payload.length < GCM_TAG_LENGTH) throw new Error("Ciphertext too short");
|
|
308
|
+
|
|
309
|
+
const roundKeys = expandKey(key);
|
|
310
|
+
const ciphertext = payload.subarray(0, payload.length - GCM_TAG_LENGTH);
|
|
311
|
+
const plaintext = new Uint8Array(ciphertext.length);
|
|
312
|
+
const counter = new Uint8Array(16);
|
|
313
|
+
const keystream = new Uint8Array(16);
|
|
314
|
+
counter.set(iv);
|
|
315
|
+
counter[15] = 1;
|
|
316
|
+
|
|
317
|
+
for (let offset = 0; offset < ciphertext.length; offset += 16) {
|
|
318
|
+
for (let i = 15; i >= 12; i--) {
|
|
319
|
+
counter[i] = (counter[i] + 1) & 0xff;
|
|
320
|
+
if (counter[i] !== 0) break;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
keystream.set(counter);
|
|
324
|
+
encryptBlock(roundKeys, keystream);
|
|
325
|
+
|
|
326
|
+
const length = Math.min(16, ciphertext.length - offset);
|
|
327
|
+
for (let i = 0; i < length; i++) plaintext[offset + i] = ciphertext[offset + i] ^ keystream[i];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return plaintext;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Public API
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
function dataUrlToBytes(url: string) {
|
|
338
|
+
const comma = url.indexOf(",");
|
|
339
|
+
if (comma === -1) throw new Error("Not a data URL");
|
|
340
|
+
|
|
341
|
+
const payload = url.slice(comma + 1);
|
|
342
|
+
|
|
343
|
+
if (!/;base64/i.test(url.slice(0, comma))) {
|
|
344
|
+
return new TextEncoder().encode(decodeURIComponent(payload));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const binary = atob(payload);
|
|
348
|
+
const bytes = new Uint8Array(binary.length);
|
|
349
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
350
|
+
return bytes;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Decrypts the `solution`/`iv`/`salt` triple of a v1 `<webwriter-task>` and
|
|
355
|
+
* returns the answer element's solution value, or `undefined` if the task
|
|
356
|
+
* carries no (readable) solution.
|
|
357
|
+
*/
|
|
358
|
+
export function decryptLegacySolution(solution: string, iv: string, salt: string): unknown {
|
|
359
|
+
const key = pbkdf2Sha256(new TextEncoder().encode(LEGACY_PASSWORD), dataUrlToBytes(salt), PBKDF2_ITERATIONS, 32);
|
|
360
|
+
const plaintext = aesGcmDecrypt(key, dataUrlToBytes(iv), dataUrlToBytes(solution));
|
|
361
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext));
|
|
362
|
+
}
|
package/src/snippets/order.html
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<webwriter-task>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
</webwriter-
|
|
2
|
+
<webwriter-task-prompt slot="prompt"><p></p></webwriter-task-prompt>
|
|
3
|
+
<webwriter-order>
|
|
4
|
+
<webwriter-order-item></webwriter-order-item>
|
|
5
|
+
<webwriter-order-item></webwriter-order-item>
|
|
6
|
+
<webwriter-order-item></webwriter-order-item>
|
|
7
|
+
<webwriter-order-item></webwriter-order-item>
|
|
8
|
+
</webwriter-order>
|
|
9
|
+
</webwriter-task>
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { msg } from "@lit/localize";
|
|
2
|
+
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js";
|
|
3
|
+
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
|
|
4
|
+
import { LitElementWw, type OptionDeclaration } from "@webwriter/lit";
|
|
5
|
+
import { css, html, nothing } from "lit";
|
|
6
|
+
import { customElement, property } from "lit/decorators.js";
|
|
7
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
8
|
+
import GripHorizontalIcon from "../../../assets/icons/grip-horizontal.svg";
|
|
9
|
+
import GripVerticalIcon from "../../../assets/icons/grip-vertical.svg";
|
|
10
|
+
import { DraggableGridItem } from "../../lib/draggable-grid";
|
|
11
|
+
import { HintPopup } from "../../lib/hint-popup";
|
|
12
|
+
import { commonStyles } from "../../lib/utils";
|
|
13
|
+
import type { WebwriterChoice } from "./webwriter-choice";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A single option of a `<webwriter-choice>`.
|
|
17
|
+
*
|
|
18
|
+
* It must be assigned to the default slot of its parent element. An optional
|
|
19
|
+
* `<webwriter-quiz-hint>` may be assigned to its `hint` slot.
|
|
20
|
+
*/
|
|
21
|
+
@customElement("webwriter-choice-item")
|
|
22
|
+
export class WebwriterChoiceItem extends LitElementWw implements DraggableGridItem {
|
|
23
|
+
/** @internal */
|
|
24
|
+
get dynamicOptions(): Record<string, OptionDeclaration> {
|
|
25
|
+
return {
|
|
26
|
+
"feedback-selected": { type: "string", label: { _: msg("Feedback if selected") } },
|
|
27
|
+
"feedback-not-selected": { type: "string", label: { _: msg("Feedback if not selected") } },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
static scopedElements = {
|
|
33
|
+
"sl-icon": SlIcon,
|
|
34
|
+
"sl-icon-button": SlIconButton,
|
|
35
|
+
"hint-popup": HintPopup,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** @internal */
|
|
39
|
+
get draggableElement() {
|
|
40
|
+
return this.shadowRoot?.querySelector(".choice-item") as HTMLElement;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static styles = [
|
|
44
|
+
commonStyles,
|
|
45
|
+
css`
|
|
46
|
+
:host {
|
|
47
|
+
display: contents !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.marker {
|
|
51
|
+
/* Mimics the appearance of Shoelace's checkbox and radio inputs as closely as possible */
|
|
52
|
+
width: 1.125em;
|
|
53
|
+
height: 1.125em;
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
|
|
56
|
+
padding: 0;
|
|
57
|
+
display: grid;
|
|
58
|
+
place-items: center;
|
|
59
|
+
|
|
60
|
+
font: inherit;
|
|
61
|
+
border: solid var(--sl-input-border-width) var(--color-border);
|
|
62
|
+
color: var(--sl-color-neutral-0);
|
|
63
|
+
background-color: var(--color-marker);
|
|
64
|
+
transition:
|
|
65
|
+
var(--sl-transition-fast) border-color,
|
|
66
|
+
var(--sl-transition-fast) background-color;
|
|
67
|
+
cursor: inherit;
|
|
68
|
+
|
|
69
|
+
.mode-single & {
|
|
70
|
+
border-radius: var(--sl-border-radius-circle);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.mode-multiple & {
|
|
74
|
+
border-radius: var(--sl-border-radius-small);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.choice-item {
|
|
79
|
+
background-color: var(--sl-color-neutral-0);
|
|
80
|
+
--color-border: var(--sl-input-border-color);
|
|
81
|
+
--color-marker: var(--sl-input-background-color);
|
|
82
|
+
--color-background: var(--sl-color-neutral-0);
|
|
83
|
+
|
|
84
|
+
&.selected {
|
|
85
|
+
--color-border: var(--sl-color-primary-600);
|
|
86
|
+
--color-marker: var(--sl-color-primary-600);
|
|
87
|
+
--color-background: var(--sl-color-primary-50);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&.feedback {
|
|
91
|
+
&.correct.selected {
|
|
92
|
+
--color-border: var(--sl-color-success-600);
|
|
93
|
+
--color-marker: var(--sl-color-success-600);
|
|
94
|
+
--color-background: var(--sl-color-success-50);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
&:not(.correct):not(.selected) {
|
|
98
|
+
--color-border: var(--sl-color-warning-600);
|
|
99
|
+
--color-marker: var(--sl-color-warning-50);
|
|
100
|
+
--color-background: var(--sl-color-warning-50);
|
|
101
|
+
|
|
102
|
+
.marker sl-icon {
|
|
103
|
+
display: none;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&:not(.correct).selected {
|
|
108
|
+
--color-border: var(--sl-color-danger-600);
|
|
109
|
+
--color-marker: var(--sl-color-danger-600);
|
|
110
|
+
--color-background: var(--sl-color-danger-50);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.active .marker-container,
|
|
116
|
+
:host(:not([contenteditable="true"]):not([contenteditable=""])) .active {
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
|
|
119
|
+
&:hover {
|
|
120
|
+
--color-border: var(--sl-input-border-color-hover);
|
|
121
|
+
--color-marker: var(--sl-input-background-color-hover);
|
|
122
|
+
|
|
123
|
+
.selected &,
|
|
124
|
+
&.selected {
|
|
125
|
+
--color-border: var(--sl-color-primary-500);
|
|
126
|
+
--color-marker: var(--sl-color-primary-500);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
slot {
|
|
132
|
+
display: block;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.feedback-message {
|
|
136
|
+
color: var(--sl-color-neutral-500);
|
|
137
|
+
font-size: var(--sl-font-size-small);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.actions {
|
|
141
|
+
height: 1lh;
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
opacity: 0;
|
|
145
|
+
|
|
146
|
+
.drag-handle {
|
|
147
|
+
cursor: grab;
|
|
148
|
+
touch-action: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
sl-icon-button::part(base) {
|
|
152
|
+
padding: 0.25em;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
hint-popup::part(empty) {
|
|
157
|
+
opacity: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
:is(.ww-dragging, :host(:hover)) .actions {
|
|
161
|
+
opacity: 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:is(.ww-dragging, :host(.ww-selected-text-within), :host(:hover)) hint-popup::part(empty) {
|
|
165
|
+
opacity: 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.layout-list {
|
|
169
|
+
display: grid !important;
|
|
170
|
+
grid-column: 1 / -1;
|
|
171
|
+
grid-template-columns: subgrid;
|
|
172
|
+
|
|
173
|
+
> * {
|
|
174
|
+
padding: var(--sl-spacing-2x-small) 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.marker-container {
|
|
178
|
+
height: 1lh;
|
|
179
|
+
padding-right: var(--sl-spacing-small);
|
|
180
|
+
|
|
181
|
+
display: flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.layout-tiles {
|
|
187
|
+
border: 1px solid var(--sl-input-border-color);
|
|
188
|
+
border-radius: var(--sl-border-radius-large);
|
|
189
|
+
padding: var(--sl-spacing-small);
|
|
190
|
+
box-sizing: border-box;
|
|
191
|
+
|
|
192
|
+
display: grid !important;
|
|
193
|
+
grid-template-columns: auto 1fr auto;
|
|
194
|
+
grid-template-rows: auto 1fr;
|
|
195
|
+
grid-template-areas: "marker . hint" "content content content";
|
|
196
|
+
position: relative;
|
|
197
|
+
aspect-ratio: 1 / 1;
|
|
198
|
+
|
|
199
|
+
:host(:not([contenteditable="true"]):not([contenteditable=""])) & {
|
|
200
|
+
border-color: var(--color-border);
|
|
201
|
+
background-color: var(--color-background);
|
|
202
|
+
transition:
|
|
203
|
+
var(--sl-transition-fast) border-color,
|
|
204
|
+
var(--sl-transition-fast) background-color;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.marker-container {
|
|
208
|
+
grid-area: marker;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hint-popup {
|
|
212
|
+
grid-area: hint;
|
|
213
|
+
translate: var(--sl-spacing-2x-small), var(--sl-spacing-2x-small);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.content {
|
|
217
|
+
grid-area: content;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.actions {
|
|
221
|
+
position: absolute;
|
|
222
|
+
background: var(--sl-color-neutral-0);
|
|
223
|
+
border: 1px solid var(--sl-input-border-color);
|
|
224
|
+
border-radius: var(--sl-border-radius-pill);
|
|
225
|
+
top: 0;
|
|
226
|
+
left: 50%;
|
|
227
|
+
translate: -50% -50%;
|
|
228
|
+
padding: 0 var(--sl-spacing-x-small);
|
|
229
|
+
box-sizing: border-box;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
`,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The feedback shown after submitting if learners selected this option.
|
|
237
|
+
*/
|
|
238
|
+
@property({ type: String, attribute: "feedback-selected", reflect: true })
|
|
239
|
+
accessor feedbackSelected: string = "";
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The feedback shown after submitting if learners did not select this option.
|
|
243
|
+
*/
|
|
244
|
+
@property({ type: String, attribute: "feedback-not-selected", reflect: true })
|
|
245
|
+
accessor feedbackNotSelected: string = "";
|
|
246
|
+
|
|
247
|
+
private dispatchSelectEvent() {
|
|
248
|
+
/** @internal */
|
|
249
|
+
this.dispatchEvent(new CustomEvent("select", { detail: { id: this.id }, bubbles: true }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private handleClick() {
|
|
253
|
+
if (!this.isContentEditable) this.dispatchSelectEvent();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
connectedCallback(): void {
|
|
257
|
+
super.connectedCallback();
|
|
258
|
+
this.addEventListener("click", this.handleClick);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
disconnectedCallback(): void {
|
|
262
|
+
super.disconnectedCallback();
|
|
263
|
+
this.removeEventListener("click", this.handleClick);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
render() {
|
|
267
|
+
const parent = this.parentElement as WebwriterChoice | undefined;
|
|
268
|
+
if (!parent) return nothing;
|
|
269
|
+
const state = parent.getChoiceState(this.id);
|
|
270
|
+
|
|
271
|
+
return html`
|
|
272
|
+
<div
|
|
273
|
+
class=${classMap({
|
|
274
|
+
"choice-item": true,
|
|
275
|
+
[`mode-${parent.mode}`]: true,
|
|
276
|
+
[`layout-${parent.layout}`]: true,
|
|
277
|
+
selected: state.selected,
|
|
278
|
+
active: !state.disabled,
|
|
279
|
+
feedback: state.showFeedback,
|
|
280
|
+
correct: state.showFeedback && state.correct,
|
|
281
|
+
})}
|
|
282
|
+
>
|
|
283
|
+
<div class="marker-container" @click=${() => this.isContentEditable && this.dispatchSelectEvent()}>
|
|
284
|
+
<button
|
|
285
|
+
class=${classMap({
|
|
286
|
+
marker: true,
|
|
287
|
+
correct: state.correct,
|
|
288
|
+
})}
|
|
289
|
+
?disabled=${state.disabled}
|
|
290
|
+
>
|
|
291
|
+
<sl-icon library="system" name=${parent.mode === "multiple" ? "check" : "radio"}></sl-icon>
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div class="content">
|
|
296
|
+
<slot style="--ww-placeholder: '${"Option"}'"></slot>
|
|
297
|
+
${state.showFeedback
|
|
298
|
+
? html`<div class="feedback-message">
|
|
299
|
+
${state?.correct ? this.feedbackSelected : this.feedbackNotSelected}
|
|
300
|
+
</div> `
|
|
301
|
+
: nothing}
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<div class="actions author-only">
|
|
305
|
+
<sl-icon-button
|
|
306
|
+
class="drag-handle"
|
|
307
|
+
data-drag-handle
|
|
308
|
+
src=${parent.layout === "list" ? GripVerticalIcon : GripHorizontalIcon}
|
|
309
|
+
></sl-icon-button>
|
|
310
|
+
</div>
|
|
311
|
+
|
|
312
|
+
<hint-popup position="start"><slot name="hint"></slot></hint-popup>
|
|
313
|
+
</div>
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
}
|