cloudiamond-scratch 0.1.0__tar.gz

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.
Files changed (37) hide show
  1. cloudiamond_scratch-0.1.0/LICENSE +21 -0
  2. cloudiamond_scratch-0.1.0/MANIFEST.in +4 -0
  3. cloudiamond_scratch-0.1.0/PKG-INFO +191 -0
  4. cloudiamond_scratch-0.1.0/README.md +168 -0
  5. cloudiamond_scratch-0.1.0/examples/demo_loopback.py +37 -0
  6. cloudiamond_scratch-0.1.0/pyproject.toml +34 -0
  7. cloudiamond_scratch-0.1.0/pytest.ini +4 -0
  8. cloudiamond_scratch-0.1.0/setup.cfg +4 -0
  9. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/__init__.py +7 -0
  10. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/__main__.py +5 -0
  11. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/bridge.py +84 -0
  12. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/cli.py +113 -0
  13. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/py.typed +0 -0
  14. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/reference/__init__.py +1 -0
  15. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/reference/client.py +113 -0
  16. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/reference/prims.py +378 -0
  17. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/scratch/__init__.py +0 -0
  18. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/scratch/build.py +97 -0
  19. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/scratch/program.py +1030 -0
  20. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/scratch/spy.py +694 -0
  21. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/scratch/spyrt.py +140 -0
  22. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/selftest.py +125 -0
  23. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch/server.py +288 -0
  24. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/PKG-INFO +191 -0
  25. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/SOURCES.txt +35 -0
  26. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/dependency_links.txt +1 -0
  27. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/entry_points.txt +2 -0
  28. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/requires.txt +5 -0
  29. cloudiamond_scratch-0.1.0/src/cloudiamond_scratch.egg-info/top_level.txt +1 -0
  30. cloudiamond_scratch-0.1.0/tests/spy_sample.py +86 -0
  31. cloudiamond_scratch-0.1.0/tests/test_core.py +426 -0
  32. cloudiamond_scratch-0.1.0/tests/test_package.py +224 -0
  33. cloudiamond_scratch-0.1.0/tests/test_program_py.py +408 -0
  34. cloudiamond_scratch-0.1.0/tests/test_program_vm.py +331 -0
  35. cloudiamond_scratch-0.1.0/tests/test_spy.py +116 -0
  36. cloudiamond_scratch-0.1.0/tests/vm.py +40 -0
  37. cloudiamond_scratch-0.1.0/tests/vmharness.js +78 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 cloudiamond-scratch contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include README.md pytest.ini
2
+ recursive-include tests *.py *.js
3
+ recursive-include examples *.py
4
+ prune tests/node_modules
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudiamond-scratch
3
+ Version: 0.1.0
4
+ Summary: Encrypted Scratch <-> Python communication over cloud variables: X25519 + ChaCha20-Poly1305, with the Scratch client generated as an .sb3 and verified on the real Scratch VM.
5
+ License-Expression: MIT
6
+ Keywords: scratch,turbowarp,cloud variables,encryption,x25519,chacha20-poly1305,sb3
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Security :: Cryptography
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: cryptography>=42
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == "test"
21
+ Requires-Dist: pycryptodome>=3.20; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # cloudiamond-scratch
25
+
26
+ **Encrypted Scratch ⇄ Python communication that stays secure even though every Scratch project's code is public.**
27
+ Key exchange X25519, encryption ChaCha20-Poly1305 — the Scratch side is generated as an `.sb3` and has been run on the real Scratch VM (`scratch-vm`) against the real Python server.
28
+
29
+ Scratch ⇄ Python サーバー間の通信を、**コードが全員に見える**前提で守るためのパッケージです。
30
+
31
+ > `cloudiamond-scratch` は PyPI 上の別プロジェクト `cloudiamond` とは無関係です(import 名は衝突しないよう `cloudiamond_scratch` にしています)。
32
+
33
+ ## 発想
34
+
35
+ Scratch のコードは誰でも読めるので、「アルゴリズムや鍵を隠す」設計は成立しません。そこで **秘密をコードに一切置かない** ようにしています。
36
+
37
+ - サーバーの**公開鍵**だけをプロジェクトに埋め込む(公開情報)。
38
+ - クライアントは接続のたびに**使い捨ての秘密鍵**を作る。その鍵は誰にも送られない。
39
+ - プロジェクトのコード・公開鍵・通信の全記録を知っていても、鍵は計算できない(X25519 の離散対数問題)。
40
+
41
+ 暗号は標準のもの(X25519 / ChaCha20-Poly1305 RFC 8439 / HChaCha20)だけで、独自の暗号は作っていません。Scratch にはビット演算も 64bit 整数も無いので、`+ - * floor mod` とリストだけで書き直してあります。
42
+
43
+ ## インストールと最短手順
44
+
45
+ ```bash
46
+ pip install cloudiamond-scratch
47
+ cloudiamond-scratch selftest # 自己診断(数秒)
48
+ cloudiamond-scratch keygen server.key # サーバーの長期鍵。秘密鍵は公開しない
49
+ cloudiamond-scratch build --key server.key -o client.sb3
50
+ ```
51
+
52
+ `client.sb3` を Scratch / TurboWarp で開くと、`🔒 connect` / `🔒 send text` / `🔒 wait for message` などのブロックが入っています(緑の旗でデモが動きます)。埋め込まれるのは**公開鍵だけ**です(`--pub <64桁の16進>` でも生成できます)。
53
+
54
+ サーバー側(通信手段の部分は使う人が用意します):
55
+
56
+ ```python
57
+ from cloudiamond_scratch import CloudBridge, SecureServer, load_or_create_static_key
58
+
59
+ server = SecureServer(load_or_create_static_key("server.key"),
60
+ on_message=lambda sid, data: b"echo:" + data) # None を返せば無返信
61
+ bridge = CloudBridge(server, write=my_cloud_write) # write(name, value): クラウド変数を書く関数
62
+
63
+ # クラウド変数が更新されるたびに(名前と値を)渡す。slot を全部書いた後で HDR が書かれる順序を保つこと
64
+ bridge.on_variable(name, value)
65
+ bridge.push(sid, b"server-initiated message") # サーバー起点の送信(任意)
66
+ ```
67
+
68
+ `CloudBridge` は WebSocket も `scratchattach` も知りません。**接続層(Scratch / TurboWarp の クラウド変数サーバーへの接続)は含まれておらず、実際のサーバーに対しては未検証です。** その代わり、クラウド変数と同じ挙動(slot → HDR の順に書く)で、本物の Scratch VM とつないだ通しテストが付いています。
69
+
70
+ ## プロトコル
71
+
72
+ ```
73
+ Scratch (client) Python (server)
74
+ e ← 使い捨て乱数(32 byte) s = 長期秘密鍵(サーバーだけが持つ)
75
+ E = e·G S = s·G ← .sb3 に埋め込む(公開情報)
76
+
77
+ sid | 01 | E ───────────────────────────────▶ r ← 使い捨て乱数, R = r·G
78
+ dh_es = X25519(s, E) dh_ee = X25519(r, E)
79
+ r を破棄
80
+ ◀────────────── sid | 02 | R | AEAD("READY") (AAD = sid‖02‖E‖R)
81
+ dh_es = X25519(e, S) dh_ee = X25519(e, R)
82
+ AEAD を検証できたら = 相手は s を持つサーバー
83
+
84
+ 鍵: master = HChaCha20(dh_ee, HChaCha20(dh_es,"master")[:16]); k_c2s / k_s2c = HChaCha20(master, ラベル)
85
+ 以降: sid | 03(c2s)/04(s2c) | ctr(6 byte) | ChaCha20-Poly1305( 長さ‖データ‖0 を 32 byte 単位にパディング )
86
+ ```
87
+
88
+ (ラベル中の `SCRSEC1` はプロトコルのバージョン識別子で、旧称の名残です。変更すると互換性が切れるのでそのままにしています。)
89
+
90
+ | 性質 | どう実現しているか |
91
+ |---|---|
92
+ | 盗聴されても読めない | X25519 + ChaCha20-Poly1305。通信に平文も鍵も乗らない |
93
+ | サーバーのなりすまし防止 | `dh_es` は `s` を持つ者しか計算できない。偽サーバーの返信は AEAD 検証で落ちる |
94
+ | 改ざん検出 | Poly1305 タグ。1 bit でも変わればサーバーは黙って捨てる |
95
+ | 再送(リプレイ)防止 | 方向別の単調増加カウンタ |
96
+ | 前方秘匿性 | サーバー側の一時鍵 `r` を使用後に破棄。後で `s` が漏れても過去の通信は読めない |
97
+ | 長さの秘匿 | 平文を 32 byte 単位にパディング |
98
+ | 認証前に送らない | 接続に失敗したら、そのために作った鍵・乱数をすべて消す。未接続のときは送信ブロックが何もしない |
99
+
100
+ ## 防げないもの(正直に)
101
+
102
+ - **クライアントの本人確認。** コードが公開なので、誰でも正規クライアントと同じものを作れます。ユーザー認証が必要なら、パスワード等を**暗号化されたメッセージの中で**送り、サーバー側で検証してください。
103
+ - **妨害(DoS)。** クラウド変数は誰でも書けます。壊れた通信は捨てられる(安全側)だけで、届かなくすることは防げません。
104
+ - **メタデータ。** 通信の有無・回数・タイミング、32 byte 刻みの長さ、セッション ID は見えます。
105
+ - **クライアント端末の乗っ取り**(本人の端末内のメモリは守れません)。
106
+ - **弱い乱数。** Scratch の `pick random` は暗号用ではありません(下記)。
107
+
108
+ ## 乱数
109
+
110
+ 接続時に約 3 秒かけて、`pick random`・`timer` の細かい桁・マウス座標・`days since 2000` の小数下位桁を HChaCha20 で混ぜ込んで使い捨て秘密鍵を作ります。**マウスを動かしてもらう**ほど安全側です(動かさなくても動作はしますが、その場合の強さはブラウザの `Math.random` に依存します)。
111
+
112
+ ## 保存・共有のときの注意
113
+
114
+ Scratch は変数・リストの**現在値をプロジェクトに保存**します。接続した後のプロジェクトをそのまま保存・共有すると、**通信用の鍵が入ったまま**になります。保存・公開の前に必ず `🔒 wipe secrets`(または `🔒 disconnect`)ブロックを実行してください。生成直後の `.sb3` には秘密は入っていません(テストで確認)。**サーバーの秘密鍵(`server.key`)は絶対に Scratch に入れない・コミットしない**でください。
115
+
116
+ GitHub などで `.sb3` を配布するときも同じです。**生成直後のもの、または wipe してから保存したものだけ**を置いてください。接続したあとに保存した `.sb3` は置かないでください。同じ公開鍵・同じバージョン・同じ `--name`(省略時は `Cloudiamond`)なら、`cloudiamond-scratch build --pub <公開鍵>` で同じバイト列を再生成できます。配布物には `sha256`・公開鍵・バージョンを添えておくと、受け取った人が検証できます。
117
+
118
+ プロジェクトのパレットには、内部で使う変数・リストが多数並びます(関数ごとのローカル変数を含む)。邪魔でも消さないでください。
119
+
120
+ ## クラウド変数の約束
121
+
122
+ - プロジェクトのクラウド変数は 10 個ちょうど: `☁ C2S_HDR` `☁ C2S_0..3`(クライアント→サーバー)、`☁ S2C_HDR` `☁ S2C_0..3`(サーバー→クライアント)。
123
+ - 1 slot = 84 byte(数字だけの文字列。1 byte = 3 桁 + 先頭に `1`、253 桁)。1 フレーム最大 336 byte、**メッセージ本文は最大 286 byte**。
124
+ - **slot を全部書いた後で HDR を書く。HDR の値が変わったことが「slot が揃った」合図。** HDR は書くたびに必ず変わる(seq を増やす)。サーバー側の seq は**サーバー全体で単調増加**にすること(`CloudBridge` がそうしています)。
125
+ - クラウド変数は全員で共有です。**同時に複数人が使うと衝突**します。衝突は暗号が「捨てる」だけなので安全は保たれますが、再送が必要です(HELLO の自動再送はまだありません)。
126
+ - 数字だけ・256 桁まで・1 プロジェクト 10 個、という制約に合わせた設計です。書き込み頻度の制限や、公式の Scratch サーバーでの動作は、**実機では確認していません**。
127
+
128
+ ## 検証したこと
129
+
130
+ テストは `tests/` にあります(`pip install "cloudiamond-scratch[test]"` + ソースツリーで `pytest`)。
131
+
132
+ - **暗号部品:** RFC 7748 / RFC 8439 の公式ベクトル、`cryptography` との照合、巨大整数の参照実装との境界値照合(Poly1305 の最終還元など)。テストの感度は、定数を 1 つ壊した版で落ちることで確認しています。
133
+ - **Scratch の制約:** 全プリミティブが「+ − × floor mod とリストだけ」で動き、途中の値が 2^53 未満に収まること(ビット演算を禁止した検査クラス経由で実行)。
134
+ - **本物の Scratch VM 上での実行:** `.sb3` を `scratch-vm` で実行し、X25519 / HChaCha20 / ChaCha20-Poly1305 / Poly1305 の出力が標準実装と一致。さらに VM 上のクライアントと実物のサーバーを、クラウド変数と同じ挙動でつなぎ、接続 → サーバー認証 → 暗号化メッセージの往復を確認。鍵の違うサーバーは拒否され、鍵素材が残らないことも確認。
135
+ - **攻撃シナリオ:** 盗聴 / ビット反転改ざん / 再送 / 反射 / 返信の差し替え(`R := S` など)/ 偽サーバー / 位数の小さい点 / ゴミ入力 / 返信フラッド / レート制限。
136
+ - **決定的ビルド:** 同じ公開鍵からは、バイト単位で同じ `.sb3` ができます。配布された `.sb3` は、公開鍵・このパッケージの同じバージョン・同じスプライト名(`--name`)から再生成して `sha256` を突き合わせれば検証できます。
137
+
138
+ ### 実測した速さ(参考値)
139
+
140
+ 開発用サンドボックスの `scratch-vm`(Node.js、画面描画なし)での値です。
141
+
142
+ | 処理 | 所要時間 |
143
+ |---|---|
144
+ | X25519 1 回 | 約 0.5 秒 |
145
+ | 接続 → 暗号化メッセージ 2 往復(乱数収集の約 3 秒を含む) | 約 6〜10 秒 |
146
+
147
+ **実際のブラウザの Scratch・スマートフォン・古い PC での時間は計測していません。** 同じインタプリタで動くはずですが、より遅い可能性があります。接続時に X25519 を 3 回行います。
148
+
149
+ ## 未検証・限界
150
+
151
+ - 実際の Scratch 公式サーバー / TurboWarp のクラウド変数サーバー越しの動作(接続層は含まれていません)
152
+ - 実ブラウザでの速度、Scratch のエディタ上での操作感
153
+ - **第三者による暗号レビューは受けていません。** 部品はすべて標準のものですが、唯一の「組み立て」は 2 段の鍵導出(`HChaCha20` を PRF として連鎖。NaCl の `crypto_box` が HSalsa20 でやっているのと同じ発想で、`dh_es` / `dh_ee` の一方が秘密なら `master` は予測できません)です。単純な XOR 結合は `R := S` で鍵が崩壊するため採りませんでした。標準の HKDF-SHA256 が理想ですが、Scratch への SHA-256 の移植コストが大きいため見送っています。重要な情報を扱う前に、レビューを受けてください。
154
+
155
+ ## 公開前チェックリスト(PyPI へ出す人向け)
156
+
157
+ ライセンスは MIT です(`LICENSE`。著作権表示は「cloudiamond-scratch contributors」にしてあるので、個人名・組織名にしたい場合は書き換えてください)。`pyproject.toml` には、公開する人が決めて書く項目がまだ残っています。**意図的に入れていません。**
158
+
159
+ 1. **作者名・連絡先**(`authors`。メールアドレスは PyPI で公開されます)
160
+ 2. **プロジェクト URL**(`[project.urls]`。GitHub の URL など)
161
+ 3. `python -m build` → `twine check --strict dist/*` → まず **TestPyPI** で `pip install` を試す
162
+ 4. アップロードは `.github/workflows/publish.yml`(PyPI の Trusted Publishing。API トークン不要)か、API トークンを使った手動の `twine upload`。トークンは他人に渡さない
163
+
164
+ ### GitHub Actions で公開する場合
165
+
166
+ `.github/workflows/publish.yml` は、Release を公開すると PyPI へ、Actions 画面からの手動実行で TestPyPI へアップロードします。PyPI / TestPyPI の Trusted Publisher 登録(Pending publisher)には次の値を入れます。
167
+
168
+ | 項目 | PyPI | TestPyPI |
169
+ |---|---|---|
170
+ | PyPI Project Name | `cloudiamond-scratch` | `cloudiamond-scratch` |
171
+ | Owner / Repository name | GitHub のアカウント名 / リポジトリ名 | 同左 |
172
+ | Workflow name | `publish.yml` | `publish.yml` |
173
+ | Environment name | `pypi` | `testpypi` |
174
+
175
+ GitHub 側でも、リポジトリの Settings → Environments に `pypi` と `testpypi` を作ってください(`pypi` には Required reviewers を付けておくと、承認なしに公開されません)。Release のタグは `v` + バージョン(例: `v0.1.0`)にします。`pyproject.toml` のバージョンと違うと、ワークフローは公開前に止まります。
176
+
177
+ ## 開発
178
+
179
+ ```bash
180
+ pip install -e ".[test]"
181
+ pytest -m "not slow" # Node.js 不要(Python 実行版の検証)
182
+ ```
183
+
184
+ 本物の Scratch VM 上のテストには Node.js と `scratch-vm` が必要です。`scratch-vm` を入れたディレクトリの `node_modules` を環境変数で渡すと実行されます(無ければ skip されます)。
185
+
186
+ ```bash
187
+ npm install scratch-vm@5.0.300
188
+ CLOUDIAMOND_SCRATCH_VM=$PWD/node_modules pytest # slow も含めて全部
189
+ ```
190
+
191
+ `scratch/program.py` が Scratch に載るクライアント本体です。「SPy」という制限付き Python で書かれていて、同じファイルが (1) 普通の Python として動き(標準実装との照合に使う)、(2) `scratch/spy.py` で Scratch のブロックにコンパイルされます。
@@ -0,0 +1,168 @@
1
+ # cloudiamond-scratch
2
+
3
+ **Encrypted Scratch ⇄ Python communication that stays secure even though every Scratch project's code is public.**
4
+ Key exchange X25519, encryption ChaCha20-Poly1305 — the Scratch side is generated as an `.sb3` and has been run on the real Scratch VM (`scratch-vm`) against the real Python server.
5
+
6
+ Scratch ⇄ Python サーバー間の通信を、**コードが全員に見える**前提で守るためのパッケージです。
7
+
8
+ > `cloudiamond-scratch` は PyPI 上の別プロジェクト `cloudiamond` とは無関係です(import 名は衝突しないよう `cloudiamond_scratch` にしています)。
9
+
10
+ ## 発想
11
+
12
+ Scratch のコードは誰でも読めるので、「アルゴリズムや鍵を隠す」設計は成立しません。そこで **秘密をコードに一切置かない** ようにしています。
13
+
14
+ - サーバーの**公開鍵**だけをプロジェクトに埋め込む(公開情報)。
15
+ - クライアントは接続のたびに**使い捨ての秘密鍵**を作る。その鍵は誰にも送られない。
16
+ - プロジェクトのコード・公開鍵・通信の全記録を知っていても、鍵は計算できない(X25519 の離散対数問題)。
17
+
18
+ 暗号は標準のもの(X25519 / ChaCha20-Poly1305 RFC 8439 / HChaCha20)だけで、独自の暗号は作っていません。Scratch にはビット演算も 64bit 整数も無いので、`+ - * floor mod` とリストだけで書き直してあります。
19
+
20
+ ## インストールと最短手順
21
+
22
+ ```bash
23
+ pip install cloudiamond-scratch
24
+ cloudiamond-scratch selftest # 自己診断(数秒)
25
+ cloudiamond-scratch keygen server.key # サーバーの長期鍵。秘密鍵は公開しない
26
+ cloudiamond-scratch build --key server.key -o client.sb3
27
+ ```
28
+
29
+ `client.sb3` を Scratch / TurboWarp で開くと、`🔒 connect` / `🔒 send text` / `🔒 wait for message` などのブロックが入っています(緑の旗でデモが動きます)。埋め込まれるのは**公開鍵だけ**です(`--pub <64桁の16進>` でも生成できます)。
30
+
31
+ サーバー側(通信手段の部分は使う人が用意します):
32
+
33
+ ```python
34
+ from cloudiamond_scratch import CloudBridge, SecureServer, load_or_create_static_key
35
+
36
+ server = SecureServer(load_or_create_static_key("server.key"),
37
+ on_message=lambda sid, data: b"echo:" + data) # None を返せば無返信
38
+ bridge = CloudBridge(server, write=my_cloud_write) # write(name, value): クラウド変数を書く関数
39
+
40
+ # クラウド変数が更新されるたびに(名前と値を)渡す。slot を全部書いた後で HDR が書かれる順序を保つこと
41
+ bridge.on_variable(name, value)
42
+ bridge.push(sid, b"server-initiated message") # サーバー起点の送信(任意)
43
+ ```
44
+
45
+ `CloudBridge` は WebSocket も `scratchattach` も知りません。**接続層(Scratch / TurboWarp の クラウド変数サーバーへの接続)は含まれておらず、実際のサーバーに対しては未検証です。** その代わり、クラウド変数と同じ挙動(slot → HDR の順に書く)で、本物の Scratch VM とつないだ通しテストが付いています。
46
+
47
+ ## プロトコル
48
+
49
+ ```
50
+ Scratch (client) Python (server)
51
+ e ← 使い捨て乱数(32 byte) s = 長期秘密鍵(サーバーだけが持つ)
52
+ E = e·G S = s·G ← .sb3 に埋め込む(公開情報)
53
+
54
+ sid | 01 | E ───────────────────────────────▶ r ← 使い捨て乱数, R = r·G
55
+ dh_es = X25519(s, E) dh_ee = X25519(r, E)
56
+ r を破棄
57
+ ◀────────────── sid | 02 | R | AEAD("READY") (AAD = sid‖02‖E‖R)
58
+ dh_es = X25519(e, S) dh_ee = X25519(e, R)
59
+ AEAD を検証できたら = 相手は s を持つサーバー
60
+
61
+ 鍵: master = HChaCha20(dh_ee, HChaCha20(dh_es,"master")[:16]); k_c2s / k_s2c = HChaCha20(master, ラベル)
62
+ 以降: sid | 03(c2s)/04(s2c) | ctr(6 byte) | ChaCha20-Poly1305( 長さ‖データ‖0 を 32 byte 単位にパディング )
63
+ ```
64
+
65
+ (ラベル中の `SCRSEC1` はプロトコルのバージョン識別子で、旧称の名残です。変更すると互換性が切れるのでそのままにしています。)
66
+
67
+ | 性質 | どう実現しているか |
68
+ |---|---|
69
+ | 盗聴されても読めない | X25519 + ChaCha20-Poly1305。通信に平文も鍵も乗らない |
70
+ | サーバーのなりすまし防止 | `dh_es` は `s` を持つ者しか計算できない。偽サーバーの返信は AEAD 検証で落ちる |
71
+ | 改ざん検出 | Poly1305 タグ。1 bit でも変わればサーバーは黙って捨てる |
72
+ | 再送(リプレイ)防止 | 方向別の単調増加カウンタ |
73
+ | 前方秘匿性 | サーバー側の一時鍵 `r` を使用後に破棄。後で `s` が漏れても過去の通信は読めない |
74
+ | 長さの秘匿 | 平文を 32 byte 単位にパディング |
75
+ | 認証前に送らない | 接続に失敗したら、そのために作った鍵・乱数をすべて消す。未接続のときは送信ブロックが何もしない |
76
+
77
+ ## 防げないもの(正直に)
78
+
79
+ - **クライアントの本人確認。** コードが公開なので、誰でも正規クライアントと同じものを作れます。ユーザー認証が必要なら、パスワード等を**暗号化されたメッセージの中で**送り、サーバー側で検証してください。
80
+ - **妨害(DoS)。** クラウド変数は誰でも書けます。壊れた通信は捨てられる(安全側)だけで、届かなくすることは防げません。
81
+ - **メタデータ。** 通信の有無・回数・タイミング、32 byte 刻みの長さ、セッション ID は見えます。
82
+ - **クライアント端末の乗っ取り**(本人の端末内のメモリは守れません)。
83
+ - **弱い乱数。** Scratch の `pick random` は暗号用ではありません(下記)。
84
+
85
+ ## 乱数
86
+
87
+ 接続時に約 3 秒かけて、`pick random`・`timer` の細かい桁・マウス座標・`days since 2000` の小数下位桁を HChaCha20 で混ぜ込んで使い捨て秘密鍵を作ります。**マウスを動かしてもらう**ほど安全側です(動かさなくても動作はしますが、その場合の強さはブラウザの `Math.random` に依存します)。
88
+
89
+ ## 保存・共有のときの注意
90
+
91
+ Scratch は変数・リストの**現在値をプロジェクトに保存**します。接続した後のプロジェクトをそのまま保存・共有すると、**通信用の鍵が入ったまま**になります。保存・公開の前に必ず `🔒 wipe secrets`(または `🔒 disconnect`)ブロックを実行してください。生成直後の `.sb3` には秘密は入っていません(テストで確認)。**サーバーの秘密鍵(`server.key`)は絶対に Scratch に入れない・コミットしない**でください。
92
+
93
+ GitHub などで `.sb3` を配布するときも同じです。**生成直後のもの、または wipe してから保存したものだけ**を置いてください。接続したあとに保存した `.sb3` は置かないでください。同じ公開鍵・同じバージョン・同じ `--name`(省略時は `Cloudiamond`)なら、`cloudiamond-scratch build --pub <公開鍵>` で同じバイト列を再生成できます。配布物には `sha256`・公開鍵・バージョンを添えておくと、受け取った人が検証できます。
94
+
95
+ プロジェクトのパレットには、内部で使う変数・リストが多数並びます(関数ごとのローカル変数を含む)。邪魔でも消さないでください。
96
+
97
+ ## クラウド変数の約束
98
+
99
+ - プロジェクトのクラウド変数は 10 個ちょうど: `☁ C2S_HDR` `☁ C2S_0..3`(クライアント→サーバー)、`☁ S2C_HDR` `☁ S2C_0..3`(サーバー→クライアント)。
100
+ - 1 slot = 84 byte(数字だけの文字列。1 byte = 3 桁 + 先頭に `1`、253 桁)。1 フレーム最大 336 byte、**メッセージ本文は最大 286 byte**。
101
+ - **slot を全部書いた後で HDR を書く。HDR の値が変わったことが「slot が揃った」合図。** HDR は書くたびに必ず変わる(seq を増やす)。サーバー側の seq は**サーバー全体で単調増加**にすること(`CloudBridge` がそうしています)。
102
+ - クラウド変数は全員で共有です。**同時に複数人が使うと衝突**します。衝突は暗号が「捨てる」だけなので安全は保たれますが、再送が必要です(HELLO の自動再送はまだありません)。
103
+ - 数字だけ・256 桁まで・1 プロジェクト 10 個、という制約に合わせた設計です。書き込み頻度の制限や、公式の Scratch サーバーでの動作は、**実機では確認していません**。
104
+
105
+ ## 検証したこと
106
+
107
+ テストは `tests/` にあります(`pip install "cloudiamond-scratch[test]"` + ソースツリーで `pytest`)。
108
+
109
+ - **暗号部品:** RFC 7748 / RFC 8439 の公式ベクトル、`cryptography` との照合、巨大整数の参照実装との境界値照合(Poly1305 の最終還元など)。テストの感度は、定数を 1 つ壊した版で落ちることで確認しています。
110
+ - **Scratch の制約:** 全プリミティブが「+ − × floor mod とリストだけ」で動き、途中の値が 2^53 未満に収まること(ビット演算を禁止した検査クラス経由で実行)。
111
+ - **本物の Scratch VM 上での実行:** `.sb3` を `scratch-vm` で実行し、X25519 / HChaCha20 / ChaCha20-Poly1305 / Poly1305 の出力が標準実装と一致。さらに VM 上のクライアントと実物のサーバーを、クラウド変数と同じ挙動でつなぎ、接続 → サーバー認証 → 暗号化メッセージの往復を確認。鍵の違うサーバーは拒否され、鍵素材が残らないことも確認。
112
+ - **攻撃シナリオ:** 盗聴 / ビット反転改ざん / 再送 / 反射 / 返信の差し替え(`R := S` など)/ 偽サーバー / 位数の小さい点 / ゴミ入力 / 返信フラッド / レート制限。
113
+ - **決定的ビルド:** 同じ公開鍵からは、バイト単位で同じ `.sb3` ができます。配布された `.sb3` は、公開鍵・このパッケージの同じバージョン・同じスプライト名(`--name`)から再生成して `sha256` を突き合わせれば検証できます。
114
+
115
+ ### 実測した速さ(参考値)
116
+
117
+ 開発用サンドボックスの `scratch-vm`(Node.js、画面描画なし)での値です。
118
+
119
+ | 処理 | 所要時間 |
120
+ |---|---|
121
+ | X25519 1 回 | 約 0.5 秒 |
122
+ | 接続 → 暗号化メッセージ 2 往復(乱数収集の約 3 秒を含む) | 約 6〜10 秒 |
123
+
124
+ **実際のブラウザの Scratch・スマートフォン・古い PC での時間は計測していません。** 同じインタプリタで動くはずですが、より遅い可能性があります。接続時に X25519 を 3 回行います。
125
+
126
+ ## 未検証・限界
127
+
128
+ - 実際の Scratch 公式サーバー / TurboWarp のクラウド変数サーバー越しの動作(接続層は含まれていません)
129
+ - 実ブラウザでの速度、Scratch のエディタ上での操作感
130
+ - **第三者による暗号レビューは受けていません。** 部品はすべて標準のものですが、唯一の「組み立て」は 2 段の鍵導出(`HChaCha20` を PRF として連鎖。NaCl の `crypto_box` が HSalsa20 でやっているのと同じ発想で、`dh_es` / `dh_ee` の一方が秘密なら `master` は予測できません)です。単純な XOR 結合は `R := S` で鍵が崩壊するため採りませんでした。標準の HKDF-SHA256 が理想ですが、Scratch への SHA-256 の移植コストが大きいため見送っています。重要な情報を扱う前に、レビューを受けてください。
131
+
132
+ ## 公開前チェックリスト(PyPI へ出す人向け)
133
+
134
+ ライセンスは MIT です(`LICENSE`。著作権表示は「cloudiamond-scratch contributors」にしてあるので、個人名・組織名にしたい場合は書き換えてください)。`pyproject.toml` には、公開する人が決めて書く項目がまだ残っています。**意図的に入れていません。**
135
+
136
+ 1. **作者名・連絡先**(`authors`。メールアドレスは PyPI で公開されます)
137
+ 2. **プロジェクト URL**(`[project.urls]`。GitHub の URL など)
138
+ 3. `python -m build` → `twine check --strict dist/*` → まず **TestPyPI** で `pip install` を試す
139
+ 4. アップロードは `.github/workflows/publish.yml`(PyPI の Trusted Publishing。API トークン不要)か、API トークンを使った手動の `twine upload`。トークンは他人に渡さない
140
+
141
+ ### GitHub Actions で公開する場合
142
+
143
+ `.github/workflows/publish.yml` は、Release を公開すると PyPI へ、Actions 画面からの手動実行で TestPyPI へアップロードします。PyPI / TestPyPI の Trusted Publisher 登録(Pending publisher)には次の値を入れます。
144
+
145
+ | 項目 | PyPI | TestPyPI |
146
+ |---|---|---|
147
+ | PyPI Project Name | `cloudiamond-scratch` | `cloudiamond-scratch` |
148
+ | Owner / Repository name | GitHub のアカウント名 / リポジトリ名 | 同左 |
149
+ | Workflow name | `publish.yml` | `publish.yml` |
150
+ | Environment name | `pypi` | `testpypi` |
151
+
152
+ GitHub 側でも、リポジトリの Settings → Environments に `pypi` と `testpypi` を作ってください(`pypi` には Required reviewers を付けておくと、承認なしに公開されません)。Release のタグは `v` + バージョン(例: `v0.1.0`)にします。`pyproject.toml` のバージョンと違うと、ワークフローは公開前に止まります。
153
+
154
+ ## 開発
155
+
156
+ ```bash
157
+ pip install -e ".[test]"
158
+ pytest -m "not slow" # Node.js 不要(Python 実行版の検証)
159
+ ```
160
+
161
+ 本物の Scratch VM 上のテストには Node.js と `scratch-vm` が必要です。`scratch-vm` を入れたディレクトリの `node_modules` を環境変数で渡すと実行されます(無ければ skip されます)。
162
+
163
+ ```bash
164
+ npm install scratch-vm@5.0.300
165
+ CLOUDIAMOND_SCRATCH_VM=$PWD/node_modules pytest # slow も含めて全部
166
+ ```
167
+
168
+ `scratch/program.py` が Scratch に載るクライアント本体です。「SPy」という制限付き Python で書かれていて、同じファイルが (1) 普通の Python として動き(標準実装との照合に使う)、(2) `scratch/spy.py` で Scratch のブロックにコンパイルされます。
@@ -0,0 +1,37 @@
1
+ """
2
+ demo_loopback.py -- ネットワーク無しで 1 往復を再現し、「盗聴者に見えるもの」を表示する。
3
+ 実行: python demo_loopback.py
4
+ """
5
+ import os
6
+
7
+ from cloudiamond_scratch.reference import client as cr
8
+ import cloudiamond_scratch.server as ss
9
+
10
+ wire = [] # 盗聴者が見ているクラウド変数の全履歴
11
+
12
+
13
+ def tap(direction, frame, seq):
14
+ slots, hdr = ss.to_slots(frame, seq)
15
+ wire.append((direction, hdr, slots))
16
+ return ss.from_slots(hdr, slots)[1] # 受け手が受け取る内容(= 盗聴者と同じもの)
17
+
18
+
19
+ server = ss.SecureServer(on_message=lambda sid, d: b"your balance is 1234 coins" if d == b"balance?" else b"?")
20
+ print("サーバー公開鍵(Scratch に埋め込む・公開OK):", ss.to_digits(server.public_key)[:40], "...")
21
+
22
+ client = cr.ScratchClient(list(server.public_key), cr.mix_entropy(list(os.urandom(32)), list(os.urandom(32))))
23
+ reply = server.handle_frame(tap("C→S", bytes(client.hello()), 1))[0]
24
+ assert client.on_reply(list(tap("S→C", reply, 1)))
25
+ print("ハンドシェイク完了(サーバー認証 + 前方秘匿性)\n")
26
+
27
+ for i, text in enumerate([b"balance?", b"password=hunter2"], start=2):
28
+ resp = server.handle_frame(tap("C→S", bytes(client.seal(list(text))), i))[0]
29
+ print("クライアントが送った平文 :", text)
30
+ print("クライアントが受け取った :", bytes(client.open(list(tap("S→C", resp, i)))), "\n")
31
+
32
+ print("=" * 70)
33
+ print("盗聴者に見えるクラウド変数の履歴(全部です):")
34
+ for d, hdr, slots in wire:
35
+ print(f" {d} HDR={hdr} slots={len(slots)} 先頭slot={slots[0][:48]}...")
36
+ plain = b"".join(ss.from_digits(s) for _, _, sl in wire for s in sl)
37
+ print("\n平文 'hunter2' が通信に含まれるか:", b"hunter2" in plain, "/ 'balance' が含まれるか:", b"balance" in plain)
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"] # 77 以上: license = "<SPDX式>" と license-files に対応
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "cloudiamond-scratch"
7
+ version = "0.1.0"
8
+ description = "Encrypted Scratch <-> Python communication over cloud variables: X25519 + ChaCha20-Poly1305, with the Scratch client generated as an .sb3 and verified on the real Scratch VM."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["cryptography>=42"]
14
+ keywords = ["scratch", "turbowarp", "cloud variables", "encryption", "x25519", "chacha20-poly1305", "sb3"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Education",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Security :: Cryptography",
24
+ ]
25
+ # authors / urls は公開する人が決めて追記する(README の「公開前チェックリスト」参照)
26
+
27
+ [project.optional-dependencies]
28
+ test = ["pytest>=8", "pycryptodome>=3.20"]
29
+
30
+ [project.scripts]
31
+ cloudiamond-scratch = "cloudiamond_scratch.cli:main"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [pytest]
2
+ testpaths = tests
3
+ markers =
4
+ slow: 本物の Scratch VM 上で X25519 を何度も走らせる遅いテスト(-m "not slow" で除外)
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """cloudiamond-scratch: Scratch <-> Python の暗号通信(X25519 + ChaCha20-Poly1305)。"""
2
+ __version__ = "0.1.0"
3
+
4
+ from .bridge import CloudBridge # noqa: E402
5
+ from .server import SecureServer, load_or_create_static_key, public_bytes # noqa: E402
6
+
7
+ __all__ = ["CloudBridge", "SecureServer", "load_or_create_static_key", "public_bytes", "__version__"]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ """
2
+ bridge.py -- クラウド変数(Scratch / TurboWarp)と SecureServer をつなぐ、通信手段に依存しない橋渡し。
3
+
4
+ CloudBridge は WebSocket も scratchattach も知らない。使う側が次の 2 つだけ用意する。
5
+ on_variable(name, value) クラウド変数が更新されたら呼ぶ(受信)
6
+ write(name, value) クラウド変数を書く関数を渡す(送信。slot を全部書いた「後で」HDR を書く)
7
+
8
+ Scratch 側(scratch/program.py)との約束:
9
+ C2S = クライアント -> サーバー: "☁ C2S_0".."☁ C2S_3"(slot) と "☁ C2S_HDR"(コミット)
10
+ S2C = サーバー -> クライアント: "☁ S2C_0".."☁ S2C_3" と "☁ S2C_HDR"
11
+ HDR の値が「変わった」ことが「slot が揃った」合図。だから HDR は書くたびに必ず変わる(seq を増やす)。
12
+ seq はサーバー全体で単調増加させる。セッションごとに 1 から数え直すと、新しい接続の最初の返信の HDR が
13
+ 前の接続の最後の HDR と同じ値になり、クライアントが「変化なし」と見なしてしまう。
14
+
15
+ 安全性: クラウド変数は誰でも書ける。壊れた値・他人の値は暗号(AEAD)の検証で必ず捨てられる(= 安全側)。
16
+ 捨てられるだけで届かなくはできる(妨害は防げない)。
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import random
21
+ import threading
22
+ from typing import Callable, Dict, List, Optional
23
+
24
+ from .server import MAX_SLOTS, SecureServer, from_slots, to_slots
25
+
26
+ C2S_HDR = "☁ C2S_HDR"
27
+ C2S_SLOTS = ["☁ C2S_%d" % i for i in range(MAX_SLOTS)]
28
+ S2C_HDR = "☁ S2C_HDR"
29
+ S2C_SLOTS = ["☁ S2C_%d" % i for i in range(MAX_SLOTS)]
30
+ CLOUD_VARIABLES = [C2S_HDR] + C2S_SLOTS + [S2C_HDR] + S2C_SLOTS # プロジェクトに必要な 10 個
31
+
32
+
33
+ class CloudBridge:
34
+ def __init__(self, server: SecureServer, write: Callable[[str, str], None],
35
+ seq_start: Optional[int] = None):
36
+ self.server = server
37
+ self._write = write
38
+ self._values: Dict[str, str] = {}
39
+ self._seq = seq_start if seq_start is not None else random.SystemRandom().randrange(1, 900000)
40
+ self._lock = threading.Lock()
41
+
42
+ # ---------------------------------------------------------------- 受信
43
+ def on_variable(self, name: str, value) -> None:
44
+ """クラウド変数の更新を 1 件渡す。名前が C2S_* 以外なら無視する。"""
45
+ if name not in C2S_SLOTS and name != C2S_HDR:
46
+ return
47
+ with self._lock:
48
+ self._values[name] = str(value)
49
+ if name != C2S_HDR:
50
+ return
51
+ # 同じ HDR を「重複」として捨てない: 別クライアントの正規フレームが偶然同じ HDR 値になり得るため。
52
+ # 再接続時の現在値の再送などで同じフレームが再度来ても、サーバーのリプレイ防止
53
+ # (既存 sid の HELLO・古いカウンタは拒否)が無害化する。
54
+ frames = self._decode(self._values[C2S_HDR])
55
+ if frames is None:
56
+ return
57
+ for reply in self.server.handle_frame(frames):
58
+ self._emit(reply)
59
+
60
+ def _decode(self, hdr: str) -> Optional[bytes]:
61
+ try:
62
+ n = int(hdr[-2:])
63
+ slots = [self._values[C2S_SLOTS[i]] for i in range(n)]
64
+ return from_slots(hdr, slots)[1]
65
+ except (ValueError, KeyError, IndexError):
66
+ return None # 壊れた値は黙って捨てる
67
+
68
+ # ---------------------------------------------------------------- 送信
69
+ def push(self, sid: bytes, data: bytes) -> bool:
70
+ """サーバー起点でクライアントへ送る(セッションが無い・長すぎる場合は False)。"""
71
+ frame = self.server.send(sid, data)
72
+ if frame is None:
73
+ return False
74
+ self._emit(frame)
75
+ return True
76
+
77
+ def _emit(self, frame: bytes) -> None:
78
+ with self._lock:
79
+ self._seq = self._seq % 999999 + 1 # 1..999999 を巡回。毎回必ず変わる
80
+ seq = self._seq
81
+ slots, hdr = to_slots(frame, seq)
82
+ for i, s in enumerate(slots):
83
+ self._write(S2C_SLOTS[i], s)
84
+ self._write(S2C_HDR, hdr) # HDR を最後に書く = コミット