cloudiamond-scratch 0.1.0__py3-none-any.whl
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.
- cloudiamond_scratch/__init__.py +7 -0
- cloudiamond_scratch/__main__.py +5 -0
- cloudiamond_scratch/bridge.py +84 -0
- cloudiamond_scratch/cli.py +113 -0
- cloudiamond_scratch/py.typed +0 -0
- cloudiamond_scratch/reference/__init__.py +1 -0
- cloudiamond_scratch/reference/client.py +113 -0
- cloudiamond_scratch/reference/prims.py +378 -0
- cloudiamond_scratch/scratch/__init__.py +0 -0
- cloudiamond_scratch/scratch/build.py +97 -0
- cloudiamond_scratch/scratch/program.py +1030 -0
- cloudiamond_scratch/scratch/spy.py +694 -0
- cloudiamond_scratch/scratch/spyrt.py +140 -0
- cloudiamond_scratch/selftest.py +125 -0
- cloudiamond_scratch/server.py +288 -0
- cloudiamond_scratch-0.1.0.dist-info/METADATA +191 -0
- cloudiamond_scratch-0.1.0.dist-info/RECORD +21 -0
- cloudiamond_scratch-0.1.0.dist-info/WHEEL +5 -0
- cloudiamond_scratch-0.1.0.dist-info/entry_points.txt +2 -0
- cloudiamond_scratch-0.1.0.dist-info/licenses/LICENSE +21 -0
- cloudiamond_scratch-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloudiamond_scratch.reference.prims -- Scratch で実装できる演算だけで書いた暗号プリミティブ(実行可能な仕様書)
|
|
3
|
+
|
|
4
|
+
【制約】Scratch の数値は倍精度浮動小数点(double)で、使える演算は
|
|
5
|
+
+ - * floor(a / 2^k) mod 比較 リスト(配列)アクセス
|
|
6
|
+
だけ。ビット演算(& | ^ << >>)も、64bit整数も、巨大整数もありません。
|
|
7
|
+
このファイルは「その制約の中で」次を実装します。
|
|
8
|
+
|
|
9
|
+
X25519 (RFC 7748) ... TweetNaCl の 16bit×16 limb 方式
|
|
10
|
+
ChaCha20 (RFC 8439)
|
|
11
|
+
HChaCha20 (XChaCha20 の鍵導出部)
|
|
12
|
+
Poly1305 (RFC 8439) ... 8bit×17 limb 方式
|
|
13
|
+
AEAD ChaCha20-Poly1305 (RFC 8439)
|
|
14
|
+
|
|
15
|
+
【機械的な強制】
|
|
16
|
+
Chk クラスを使うと、(1) どの中間値も 2^53 未満であること(= double で厳密)、
|
|
17
|
+
(2) ビット演算を一切使っていないこと を実行時に検査できます。
|
|
18
|
+
tests/test_core.py は全プリミティブを Chk 経由で1回ずつ実行し、検査します。
|
|
19
|
+
|
|
20
|
+
各関数は「普通の int」でも「Chk」でも動きます(同じコードをそのままブロックに移植できます)。
|
|
21
|
+
|
|
22
|
+
【Scratch への移植時の注意】
|
|
23
|
+
floor(a / 2^k) は 2 の冪での除算なので double でも厳密です(ここでは // と書いています)。
|
|
24
|
+
Scratch の mod は「符号が除数側の mod(floored mod)」で Python の % と同じ意味です。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
LIMIT = 2 ** 53 # double が整数を厳密に表せる上限
|
|
28
|
+
OPCOUNT = 0 # Chk 経由の演算回数(ブロック数の見積り用)
|
|
29
|
+
MAXSEEN = 0 # Chk 経由で観測した最大の絶対値
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _w(v):
|
|
33
|
+
global OPCOUNT, MAXSEEN
|
|
34
|
+
OPCOUNT += 1
|
|
35
|
+
a = -v if v < 0 else v
|
|
36
|
+
if a >= LIMIT:
|
|
37
|
+
raise OverflowError("intermediate value >= 2^53: Scratch(double) cannot hold it exactly")
|
|
38
|
+
if a > MAXSEEN:
|
|
39
|
+
MAXSEEN = a
|
|
40
|
+
return Chk(v)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _no_bitwise(*_a, **_k):
|
|
44
|
+
raise TypeError("Scratch has no bitwise operators")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Chk(int):
|
|
48
|
+
"""2^53 未満チェック付き整数。ビット演算・/・** は禁止。"""
|
|
49
|
+
__slots__ = ()
|
|
50
|
+
|
|
51
|
+
def __add__(s, o): return _w(int(s) + int(o))
|
|
52
|
+
__radd__ = __add__
|
|
53
|
+
def __sub__(s, o): return _w(int(s) - int(o))
|
|
54
|
+
def __rsub__(s, o): return _w(int(o) - int(s))
|
|
55
|
+
def __mul__(s, o): return _w(int(s) * int(o))
|
|
56
|
+
__rmul__ = __mul__
|
|
57
|
+
def __floordiv__(s, o): return _w(int(s) // int(o))
|
|
58
|
+
def __rfloordiv__(s, o): return _w(int(o) // int(s))
|
|
59
|
+
def __mod__(s, o): return _w(int(s) % int(o))
|
|
60
|
+
def __rmod__(s, o): return _w(int(o) % int(s))
|
|
61
|
+
def __neg__(s): return _w(-int(s))
|
|
62
|
+
__and__ = __rand__ = __or__ = __ror__ = __xor__ = __rxor__ = _no_bitwise
|
|
63
|
+
__lshift__ = __rlshift__ = __rshift__ = __rrshift__ = __invert__ = _no_bitwise
|
|
64
|
+
__truediv__ = __rtruediv__ = __pow__ = __rpow__ = _no_bitwise
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------------ bit helpers
|
|
68
|
+
def _build_xor4():
|
|
69
|
+
"""4bit XOR 表(256要素のリスト)。起動時に Scratch でも算術だけで作れる。"""
|
|
70
|
+
t = []
|
|
71
|
+
for a in range(16):
|
|
72
|
+
for b in range(16):
|
|
73
|
+
r = 0
|
|
74
|
+
for k in range(4):
|
|
75
|
+
bit = ((a // 2 ** k) % 2 + (b // 2 ** k) % 2) % 2
|
|
76
|
+
r += bit * 2 ** k
|
|
77
|
+
t.append(r)
|
|
78
|
+
return t
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
XOR4 = _build_xor4() # Scratch: リスト "XOR4" (256個)
|
|
82
|
+
P16 = [16 ** k for k in range(8)]
|
|
83
|
+
M32 = 2 ** 32
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def xor32(a, b):
|
|
87
|
+
"""32bit XOR = 4bit ずつ表引き(8回)。"""
|
|
88
|
+
r = 0
|
|
89
|
+
for k in range(8):
|
|
90
|
+
r = r + XOR4[(a // P16[k]) % 16 * 16 + (b // P16[k]) % 16] * P16[k]
|
|
91
|
+
return r
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def xor8(a, b):
|
|
95
|
+
return XOR4[a // 16 * 16 + b // 16] * 16 + XOR4[a % 16 * 16 + b % 16]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def rotl32(x, n):
|
|
99
|
+
return (x * 2 ** n) % M32 + x // 2 ** (32 - n)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def le32(b, i):
|
|
103
|
+
return b[i] + b[i + 1] * 256 + b[i + 2] * 65536 + b[i + 3] * 16777216
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def put_le32(w):
|
|
107
|
+
return [w % 256, w // 256 % 256, w // 65536 % 256, w // 16777216 % 256]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def use_checked():
|
|
111
|
+
"""定数表を Chk に差し替え、以降の全演算を 2^53 検査・ビット演算禁止の対象にする(監査用)。"""
|
|
112
|
+
global XOR4, P16, M32, _SIGMA, _MINUSP, _121665
|
|
113
|
+
XOR4 = [Chk(v) for v in XOR4]
|
|
114
|
+
P16 = [Chk(v) for v in P16]
|
|
115
|
+
M32 = Chk(M32)
|
|
116
|
+
_SIGMA = [Chk(v) for v in _SIGMA]
|
|
117
|
+
_MINUSP = [Chk(v) for v in _MINUSP]
|
|
118
|
+
_121665 = [Chk(v) for v in _121665]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ------------------------------------------------------------------ ChaCha20
|
|
122
|
+
_SIGMA = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _qr(s, a, b, c, d):
|
|
126
|
+
s[a] = (s[a] + s[b]) % M32; s[d] = rotl32(xor32(s[d], s[a]), 16)
|
|
127
|
+
s[c] = (s[c] + s[d]) % M32; s[b] = rotl32(xor32(s[b], s[c]), 12)
|
|
128
|
+
s[a] = (s[a] + s[b]) % M32; s[d] = rotl32(xor32(s[d], s[a]), 8)
|
|
129
|
+
s[c] = (s[c] + s[d]) % M32; s[b] = rotl32(xor32(s[b], s[c]), 7)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _rounds(s):
|
|
133
|
+
for _ in range(10):
|
|
134
|
+
_qr(s, 0, 4, 8, 12); _qr(s, 1, 5, 9, 13); _qr(s, 2, 6, 10, 14); _qr(s, 3, 7, 11, 15)
|
|
135
|
+
_qr(s, 0, 5, 10, 15); _qr(s, 1, 6, 11, 12); _qr(s, 2, 7, 8, 13); _qr(s, 3, 4, 9, 14)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def chacha20_block(key, counter, nonce):
|
|
139
|
+
"""key: 32要素, counter: 数値, nonce: 12要素 -> 64要素のキーストリーム"""
|
|
140
|
+
init = list(_SIGMA) + [le32(key, 4 * i) for i in range(8)] + [counter] + \
|
|
141
|
+
[le32(nonce, 4 * i) for i in range(3)]
|
|
142
|
+
s = list(init)
|
|
143
|
+
_rounds(s)
|
|
144
|
+
out = []
|
|
145
|
+
for i in range(16):
|
|
146
|
+
out += put_le32((s[i] + init[i]) % M32)
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def hchacha20(key, nonce16):
|
|
151
|
+
"""key: 32要素, nonce16: 16要素 -> 32要素(XChaCha20 の HChaCha20)"""
|
|
152
|
+
s = list(_SIGMA) + [le32(key, 4 * i) for i in range(8)] + [le32(nonce16, 4 * i) for i in range(4)]
|
|
153
|
+
_rounds(s)
|
|
154
|
+
out = []
|
|
155
|
+
for i in (0, 1, 2, 3, 12, 13, 14, 15):
|
|
156
|
+
out += put_le32(s[i])
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def chacha20_xor(key, counter, nonce, data):
|
|
161
|
+
out = []
|
|
162
|
+
n = len(data)
|
|
163
|
+
pos = 0
|
|
164
|
+
while pos < n:
|
|
165
|
+
ks = chacha20_block(key, counter, nonce)
|
|
166
|
+
counter = counter + 1
|
|
167
|
+
for j in range(64):
|
|
168
|
+
if pos >= n:
|
|
169
|
+
break
|
|
170
|
+
out.append(xor8(data[pos], ks[j]))
|
|
171
|
+
pos = pos + 1
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ------------------------------------------------------------------ Poly1305
|
|
176
|
+
_MINUSP = [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252] # 2^136 - (2^130 - 5)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _add1305(h, c):
|
|
180
|
+
u = 0
|
|
181
|
+
out = []
|
|
182
|
+
for j in range(17):
|
|
183
|
+
u = u + h[j] + c[j]
|
|
184
|
+
out.append(u % 256)
|
|
185
|
+
u = u // 256
|
|
186
|
+
return out
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def poly1305(key, msg):
|
|
190
|
+
"""key: 32要素(r||s), msg: 任意長 -> 16要素のタグ。8bit×17 limb。"""
|
|
191
|
+
r = [0] * 17
|
|
192
|
+
h = [0] * 17
|
|
193
|
+
for j in range(16):
|
|
194
|
+
r[j] = key[j]
|
|
195
|
+
for j in (3, 7, 11, 15):
|
|
196
|
+
r[j] = r[j] % 16 # & 15
|
|
197
|
+
for j in (4, 8, 12):
|
|
198
|
+
r[j] = r[j] - r[j] % 4 # & 252
|
|
199
|
+
n = len(msg)
|
|
200
|
+
pos = 0
|
|
201
|
+
while n > 0:
|
|
202
|
+
c = [0] * 17
|
|
203
|
+
j = 0
|
|
204
|
+
while j < 16 and j < n:
|
|
205
|
+
c[j] = msg[pos + j]
|
|
206
|
+
j = j + 1
|
|
207
|
+
c[j] = 1
|
|
208
|
+
pos = pos + j
|
|
209
|
+
n = n - j
|
|
210
|
+
h = _add1305(h, c)
|
|
211
|
+
x = []
|
|
212
|
+
for i in range(17):
|
|
213
|
+
acc = 0
|
|
214
|
+
for j in range(17):
|
|
215
|
+
acc = acc + h[j] * (r[i - j] if j <= i else 320 * r[i + 17 - j]) # 2^136 ≡ 320 (mod 2^130-5)
|
|
216
|
+
x.append(acc)
|
|
217
|
+
h = x
|
|
218
|
+
u = 0
|
|
219
|
+
for j in range(16):
|
|
220
|
+
u = u + h[j]
|
|
221
|
+
h[j] = u % 256
|
|
222
|
+
u = u // 256
|
|
223
|
+
u = u + h[16]
|
|
224
|
+
h[16] = u % 4
|
|
225
|
+
u = 5 * (u // 4)
|
|
226
|
+
for j in range(16):
|
|
227
|
+
u = u + h[j]
|
|
228
|
+
h[j] = u % 256
|
|
229
|
+
u = u // 256
|
|
230
|
+
u = u + h[16]
|
|
231
|
+
h[16] = u
|
|
232
|
+
g = list(h)
|
|
233
|
+
h = _add1305(h, _MINUSP)
|
|
234
|
+
if h[16] >= 128: # h - p < 0 なので元の h を採用
|
|
235
|
+
h = g
|
|
236
|
+
c = [key[16 + j] for j in range(16)] + [0]
|
|
237
|
+
h = _add1305(h, c)
|
|
238
|
+
return h[:16]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ------------------------------------------------------------------ AEAD (RFC 8439)
|
|
242
|
+
def _pad16(n):
|
|
243
|
+
return [0] * ((16 - n % 16) % 16)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _le64(n):
|
|
247
|
+
return [n // 256 ** i % 256 for i in range(8)]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _mac_data(aad, ct):
|
|
251
|
+
return list(aad) + _pad16(len(aad)) + list(ct) + _pad16(len(ct)) + _le64(len(aad)) + _le64(len(ct))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def aead_seal(key, nonce, aad, pt):
|
|
255
|
+
otk = chacha20_block(key, 0, nonce)[:32]
|
|
256
|
+
ct = chacha20_xor(key, 1, nonce, pt)
|
|
257
|
+
return ct + poly1305(otk, _mac_data(aad, ct))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def aead_open(key, nonce, aad, sealed):
|
|
261
|
+
"""成功なら平文(list)、失敗なら None。"""
|
|
262
|
+
if len(sealed) < 16:
|
|
263
|
+
return None
|
|
264
|
+
ct, tag = sealed[:-16], sealed[-16:]
|
|
265
|
+
otk = chacha20_block(key, 0, nonce)[:32]
|
|
266
|
+
exp = poly1305(otk, _mac_data(aad, ct))
|
|
267
|
+
diff = 0
|
|
268
|
+
for i in range(16):
|
|
269
|
+
diff = diff + (1 if exp[i] != tag[i] else 0)
|
|
270
|
+
if diff != 0:
|
|
271
|
+
return None
|
|
272
|
+
return chacha20_xor(key, 1, nonce, ct)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ------------------------------------------------------------------ X25519 (TweetNaCl 方式)
|
|
276
|
+
# GF(2^255-19) の元を 16bit × 16 要素のリストで表す。各 limb は 2^53 に十分収まる。
|
|
277
|
+
_121665 = [0xdb41, 1] + [0] * 14
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _car(o):
|
|
281
|
+
o = list(o)
|
|
282
|
+
for i in range(16):
|
|
283
|
+
o[i] = o[i] + 65536
|
|
284
|
+
c = o[i] // 65536
|
|
285
|
+
if i < 15:
|
|
286
|
+
o[i + 1] = o[i + 1] + (c - 1)
|
|
287
|
+
else:
|
|
288
|
+
o[0] = o[0] + 38 * (c - 1)
|
|
289
|
+
o[i] = o[i] - c * 65536
|
|
290
|
+
return o
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _A(a, b): return [a[i] + b[i] for i in range(16)]
|
|
294
|
+
def _Z(a, b): return [a[i] - b[i] for i in range(16)]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _M(a, b):
|
|
298
|
+
t = [0] * 31
|
|
299
|
+
for i in range(16):
|
|
300
|
+
ai = a[i]
|
|
301
|
+
for j in range(16):
|
|
302
|
+
t[i + j] = t[i + j] + ai * b[j]
|
|
303
|
+
for i in range(15):
|
|
304
|
+
t[i] = t[i] + 38 * t[i + 16]
|
|
305
|
+
return _car(_car(t[:16]))
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _S(a): return _M(a, a)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _inv(i):
|
|
312
|
+
c = list(i)
|
|
313
|
+
for a in range(253, -1, -1):
|
|
314
|
+
c = _S(c)
|
|
315
|
+
if a != 2 and a != 4:
|
|
316
|
+
c = _M(c, i)
|
|
317
|
+
return c
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _unpack(n):
|
|
321
|
+
o = [n[2 * i] + n[2 * i + 1] * 256 for i in range(16)]
|
|
322
|
+
o[15] = o[15] % 32768
|
|
323
|
+
return o
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _pack(n):
|
|
327
|
+
t = _car(_car(_car(n)))
|
|
328
|
+
for _ in range(2):
|
|
329
|
+
m = [0] * 16
|
|
330
|
+
m[0] = t[0] - 0xffed
|
|
331
|
+
for i in range(1, 15):
|
|
332
|
+
m[i] = t[i] - 0xffff - (1 if m[i - 1] < 0 else 0)
|
|
333
|
+
m[i - 1] = m[i - 1] % 65536
|
|
334
|
+
m[15] = t[15] - 0x7fff - (1 if m[14] < 0 else 0)
|
|
335
|
+
b = 1 if m[15] < 0 else 0
|
|
336
|
+
m[14] = m[14] % 65536
|
|
337
|
+
if b == 0:
|
|
338
|
+
t = m
|
|
339
|
+
o = []
|
|
340
|
+
for i in range(16):
|
|
341
|
+
o.append(t[i] % 256)
|
|
342
|
+
o.append(t[i] // 256)
|
|
343
|
+
return o
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def x25519(scalar, point):
|
|
347
|
+
"""scalar: 32要素, point: 32要素(u座標) -> 32要素。Montgomery ladder 255 回。"""
|
|
348
|
+
z = list(scalar)
|
|
349
|
+
z[31] = z[31] % 64 + 64 # (z31 & 127) | 64
|
|
350
|
+
z[0] = z[0] - z[0] % 8 # z0 & 248
|
|
351
|
+
x = _unpack(point)
|
|
352
|
+
zero = [0] * 16
|
|
353
|
+
one = [1] + [0] * 15
|
|
354
|
+
a, b, c, d = list(one), list(x), list(zero), list(one)
|
|
355
|
+
for i in range(254, -1, -1):
|
|
356
|
+
r = (z[i // 8] // 2 ** (i % 8)) % 2
|
|
357
|
+
if r == 1:
|
|
358
|
+
a, b = b, a
|
|
359
|
+
c, d = d, c
|
|
360
|
+
e = _A(a, c); a = _Z(a, c); c = _A(b, d); b = _Z(b, d)
|
|
361
|
+
d = _S(e); f = _S(a)
|
|
362
|
+
a = _M(c, a); c = _M(b, e)
|
|
363
|
+
e = _A(a, c); a = _Z(a, c)
|
|
364
|
+
b = _S(a); c = _Z(d, f)
|
|
365
|
+
a = _M(c, _121665); a = _A(a, d)
|
|
366
|
+
c = _M(c, a); a = _M(d, f)
|
|
367
|
+
d = _M(b, x); b = _S(e)
|
|
368
|
+
if r == 1:
|
|
369
|
+
a, b = b, a
|
|
370
|
+
c, d = d, c
|
|
371
|
+
return _pack(_M(a, _inv(c)))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
BASEPOINT = [9] + [0] * 31
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def x25519_base(scalar):
|
|
378
|
+
return x25519(scalar, BASEPOINT)
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""build.py -- SPy ソース -> .sb3 (zip) を組み立てる。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import zipfile
|
|
7
|
+
from io import BytesIO
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from .spy import Compiler
|
|
11
|
+
|
|
12
|
+
STAGE_SVG = (b'<svg version="1.1" width="2" height="2" viewBox="-1 -1 2 2" '
|
|
13
|
+
b'xmlns="http://www.w3.org/2000/svg"></svg>')
|
|
14
|
+
ICON_SVG = (b'<svg version="1.1" width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">'
|
|
15
|
+
b'<polygon points="32,4 60,28 32,60 4,28" fill="#4C97FF" stroke="#3373CC" stroke-width="3" stroke-linejoin="round"/>'
|
|
16
|
+
b'<polygon points="4,28 60,28 32,4" fill="#85B8FF" opacity=".55"/>'
|
|
17
|
+
b'<polyline points="20,28 32,60 44,28" fill="none" stroke="#fff" stroke-width="2" opacity=".6"/></svg>')
|
|
18
|
+
|
|
19
|
+
# 画面に出すモニター(Python の変数名 -> 位置)。表示名は program.py の Var(..., "表示名") で決まる。
|
|
20
|
+
DEFAULT_MONITORS = ("SS_STATUS", "SS_READY", "SS_TEXT")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _costume(name: str, svg: bytes, cx: float, cy: float):
|
|
24
|
+
md5 = hashlib.md5(svg).hexdigest()
|
|
25
|
+
return md5, {"assetId": md5, "name": name, "md5ext": md5 + ".svg", "dataFormat": "svg",
|
|
26
|
+
"rotationCenterX": cx, "rotationCenterY": cy}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_project_json(comp: Compiler, sprite_name: str = "Cloudiamond",
|
|
30
|
+
monitors: tuple = DEFAULT_MONITORS) -> Dict[str, Any]:
|
|
31
|
+
comp.compile_all()
|
|
32
|
+
_m, stage_cos = _costume("backdrop1", STAGE_SVG, 1, 1)
|
|
33
|
+
_m, icon_cos = _costume("diamond", ICON_SVG, 32, 32)
|
|
34
|
+
stage = {
|
|
35
|
+
"isStage": True, "name": "Stage",
|
|
36
|
+
"variables": {cid: [name, 0, True] for name, cid in comp.cloud.items()},
|
|
37
|
+
"lists": {}, "broadcasts": {}, "blocks": {}, "comments": {},
|
|
38
|
+
"currentCostume": 0, "costumes": [stage_cos], "sounds": [], "volume": 100, "layerOrder": 0,
|
|
39
|
+
"tempo": 60, "videoTransparency": 50, "videoState": "on", "textToSpeechLanguage": None,
|
|
40
|
+
}
|
|
41
|
+
sprite = {
|
|
42
|
+
"isStage": False, "name": sprite_name,
|
|
43
|
+
"variables": {vid: [name, comp.var_init.get(name, 0)] for name, vid in comp.vars.items()},
|
|
44
|
+
"lists": {lid: [display, items] for _n, (lid, items, display) in comp.lists.items()},
|
|
45
|
+
"broadcasts": {}, "blocks": comp.blocks, "comments": {},
|
|
46
|
+
"currentCostume": 0, "costumes": [icon_cos], "sounds": [], "volume": 100, "layerOrder": 1,
|
|
47
|
+
"visible": True, "x": 0, "y": 0, "size": 100, "direction": 90, "draggable": False,
|
|
48
|
+
"rotationStyle": "all around",
|
|
49
|
+
}
|
|
50
|
+
mons = []
|
|
51
|
+
for k, pyname in enumerate(monitors):
|
|
52
|
+
display = comp.gvars.get(pyname)
|
|
53
|
+
if not isinstance(display, str):
|
|
54
|
+
continue
|
|
55
|
+
mons.append({"id": comp.vars[display], "mode": "default", "opcode": "data_variable",
|
|
56
|
+
"params": {"VARIABLE": display}, "spriteName": sprite_name,
|
|
57
|
+
"value": comp.var_init.get(display, 0), "width": 0, "height": 0,
|
|
58
|
+
"x": 5, "y": 5 + 27 * k, "visible": True, "sliderMin": 0, "sliderMax": 100, "isDiscrete": True})
|
|
59
|
+
return {"targets": [stage, sprite], "monitors": mons, "extensions": [],
|
|
60
|
+
"meta": {"semver": "3.0.0", "vm": "0.2.0", "agent": "cloudiamond_scratch"}}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_sb3(source: str, *, list_overrides: Optional[Dict[str, List[Any]]] = None,
|
|
64
|
+
const_overrides: Optional[Dict[str, Any]] = None, sprite_name: str = "Cloudiamond",
|
|
65
|
+
monitors: tuple = DEFAULT_MONITORS):
|
|
66
|
+
"""-> (sb3 の bytes, Compiler)。"""
|
|
67
|
+
comp = Compiler(source, list_overrides=list_overrides, const_overrides=const_overrides)
|
|
68
|
+
proj = build_project_json(comp, sprite_name, monitors)
|
|
69
|
+
buf = BytesIO()
|
|
70
|
+
# 時刻やファイル順をバイトに混ぜない = 同じ入力から必ず同じ .sb3 ができる。
|
|
71
|
+
# 配布された .sb3 を、公開鍵とこのパッケージから再生成して sha256 を突き合わせて検証できる。
|
|
72
|
+
with zipfile.ZipFile(buf, "w") as z:
|
|
73
|
+
entries = [("project.json", json.dumps(proj, ensure_ascii=False, separators=(",", ":")).encode("utf-8"))]
|
|
74
|
+
entries += sorted((hashlib.md5(svg).hexdigest() + ".svg", svg) for svg in (STAGE_SVG, ICON_SVG))
|
|
75
|
+
for name, payload in entries:
|
|
76
|
+
info = zipfile.ZipInfo(name, date_time=(2020, 1, 1, 0, 0, 0))
|
|
77
|
+
info.compress_type = zipfile.ZIP_DEFLATED
|
|
78
|
+
info.external_attr = 0o644 << 16
|
|
79
|
+
z.writestr(info, payload, compresslevel=9)
|
|
80
|
+
return buf.getvalue(), comp
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def program_source() -> str:
|
|
84
|
+
"""Scratch に載せるクライアント本体(program.py)のソース。"""
|
|
85
|
+
from . import program
|
|
86
|
+
with open(program.__file__, encoding="utf-8") as f:
|
|
87
|
+
return f.read()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_client_sb3(server_pub: bytes, sprite_name: str = "Cloudiamond") -> bytes:
|
|
91
|
+
"""サーバーの *公開鍵* を埋め込んだ Scratch クライアント(.sb3)を返す。秘密鍵は受け取らない。"""
|
|
92
|
+
if len(server_pub) != 32:
|
|
93
|
+
raise ValueError("server_pub must be 32 bytes")
|
|
94
|
+
if not any(server_pub):
|
|
95
|
+
raise ValueError("server_pub must not be all zero")
|
|
96
|
+
data, _ = build_sb3(program_source(), list_overrides={"SRV": list(server_pub)}, sprite_name=sprite_name)
|
|
97
|
+
return data
|