-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscript.js
More file actions
1930 lines (1721 loc) · 74 KB
/
Copy pathscript.js
File metadata and controls
1930 lines (1721 loc) · 74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Substitution Cipher Analysis Workbench
// Manual cryptanalysis support tool
(function() {
'use strict';
// State
let currentPhase = 1;
let currentTab = 'statistics';
let ngramMode = 2;
let highlightState = {};
let highlightColor = 'yellow';
let mapping = {};
let originalCiphertext = ''; // マッピングリセット用に元の暗号文を保持
// HTML escape function for XSS prevention
function escapeHtml(str) {
const escapeMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return str.replace(/[&<>"']/g, char => escapeMap[char]);
}
// English letter frequencies (percentage)
const ENGLISH_FREQ = {
E: 12.7, T: 9.1, A: 8.2, O: 7.5, I: 7.0, N: 6.7, S: 6.3, H: 6.1, R: 6.0,
D: 4.3, L: 4.0, C: 2.8, U: 2.8, M: 2.4, W: 2.4, F: 2.2, G: 2.0, Y: 2.0,
P: 1.9, B: 1.5, V: 1.0, K: 0.8, J: 0.15, X: 0.15, Q: 0.10, Z: 0.07
};
// Reference data for each analysis tab (merged with appendix data)
const REFERENCE_DATA = {
statistics: {
title: '英文の基本統計',
content: `
<h3>文字の出現頻度(順位)</h3>
<p><strong>E T A O I N S H R D L C U M W F G Y P B V K J X Q Z</strong></p>
<p style="font-size: 0.85rem; color: #666;">覚え方: "ETAOIN SHRDLU" は最も頻出する12文字</p>
<h3>平均的な英文の特徴</h3>
<ul>
<li>平均単語長: 約6.2文字</li>
<li>母音の割合: 約40%(A, E, I, O, U)</li>
<li>最頻出文字 E: 約12.7%</li>
<li>1文字単語は a と I のみ</li>
</ul>
<h3>頻度によるグループ分類</h3>
<table class="ref-table">
<tr><th>グループ</th><th>文字</th><th>出現確率</th></tr>
<tr><td>最頻度</td><td>E</td><td>12%</td></tr>
<tr><td>高頻度</td><td>T, A, O, I, N, S, H, R</td><td>6〜9%</td></tr>
<tr><td>中頻度</td><td>D, L, U, C, M</td><td>3〜4%</td></tr>
<tr><td>低頻度</td><td>P, F, Y, W, G, B, V</td><td>1.5〜2.8%</td></tr>
<tr><td>稀頻度</td><td>J, K, Q, X, Z</td><td>1%以下</td></tr>
</table>
<h3>各言語の頻出文字</h3>
<table class="ref-table">
<tr><th>言語</th><th>頻出文字列</th></tr>
<tr><td>英語</td><td>ETAOIN SHRDLU</td></tr>
<tr><td>スペイン語</td><td>EAOSR NIDLT</td></tr>
<tr><td>フランス語</td><td>ESAIT NRUOL</td></tr>
<tr><td>ドイツ語</td><td>ENISR ATDHU</td></tr>
<tr><td>イタリア語</td><td>EAION LRTSC</td></tr>
</table>
`
},
ic: {
title: '一致指数(IC)参考値',
content: `
<h3>一致指数とは</h3>
<p>テキストからランダムに選ばれた2つの文字が同じである確率です。</p>
<h3>IC値の目安</h3>
<table class="ref-table">
<tr><th>暗号種別</th><th>IC値</th></tr>
<tr><td>英語平文</td><td>0.0667 (6.7%)</td></tr>
<tr><td>単一換字式暗号</td><td>0.0667(平文と同じ)</td></tr>
<tr><td>ヴィジュネル暗号(短い鍵)</td><td>0.045〜0.055</td></tr>
<tr><td>ヴィジュネル暗号(長い鍵)</td><td>0.040〜0.045</td></tr>
<tr><td>完全ランダム</td><td>0.0385 (≈ 1/26)</td></tr>
</table>
<h3>各言語の一致指数</h3>
<table class="ref-table">
<tr><th>言語</th><th>IC値</th><th>言語</th><th>IC値</th></tr>
<tr><td>チェコ語</td><td>5.1%</td><td>フィンランド語</td><td>7.0%</td></tr>
<tr><td>ロシア語</td><td>5.3%</td><td>トルコ語</td><td>7.0%</td></tr>
<tr><td>ポーランド語</td><td>6.1%</td><td>スペイン語</td><td>7.2%</td></tr>
<tr><td>英語</td><td>6.7%</td><td>ドイツ語</td><td>7.3%</td></tr>
<tr><td>デンマーク語</td><td>6.7%</td><td>イタリア語</td><td>7.4%</td></tr>
<tr><td>スウェーデン語</td><td>6.8%</td><td>ポルトガル語</td><td>8.2%</td></tr>
<tr><td>フランス語</td><td>6.9%</td><td></td><td></td></tr>
</table>
<h3>判定のポイント</h3>
<ul>
<li>IC ≈ 0.067 → 単一換字式の可能性が高い</li>
<li>IC ≈ 0.038〜0.045 → 多表式暗号の可能性</li>
<li>短い暗号文ではICの信頼性が低下</li>
</ul>
`
},
spacing: {
title: '英語の単語長分布',
content: `
<h3>単語長の分布</h3>
<table class="ref-table">
<tr><th>単語長</th><th>割合</th><th>代表例</th></tr>
<tr><td>1文字</td><td>約2%</td><td>a, I</td></tr>
<tr><td>2文字</td><td>約17%</td><td>to, of, in, it, is</td></tr>
<tr><td>3文字</td><td>約20%</td><td>the, and, for, are</td></tr>
<tr><td>4文字</td><td>約17%</td><td>that, with, have</td></tr>
<tr><td>5文字</td><td>約12%</td><td>their, about, which</td></tr>
<tr><td>6文字以上</td><td>約32%</td><td>(様々)</td></tr>
</table>
<h3>各言語の平均単語長</h3>
<table class="ref-table">
<tr><th>言語</th><th>平均</th><th>言語</th><th>平均</th></tr>
<tr><td>フィンランド語</td><td>7.6文字</td><td>ドイツ語</td><td>6.0文字</td></tr>
<tr><td>ロシア語</td><td>6.6文字</td><td>スウェーデン語</td><td>6.0文字</td></tr>
<tr><td>イタリア語</td><td>6.5文字</td><td>フランス語</td><td>6.0文字</td></tr>
<tr><td>英語</td><td>6.2文字</td><td>スペイン語</td><td>5.8文字</td></tr>
</table>
<h3>注意点</h3>
<p>暗号文にスペースがある場合、それが本物の単語区切りかどうかを確認する必要があります。偽のスペースが挿入されている可能性もあります。</p>
`
},
repeated: {
title: '繰り返しパターンの参考情報',
content: `
<h3>よくある繰り返しパターン</h3>
<ul>
<li><strong>the</strong>: 英語で最も頻出する3文字パターン</li>
<li><strong>and</strong>: 2番目に多い3文字パターン</li>
<li><strong>ing</strong>: 語尾としてよく出現</li>
<li><strong>tion</strong>: 名詞化接尾辞</li>
<li><strong>ment</strong>: 名詞化接尾辞</li>
</ul>
<h3>パターンの解釈</h3>
<table class="ref-table">
<tr><th>パターン長</th><th>可能性</th></tr>
<tr><td>3文字</td><td>the, and, for, are, tha, ent, ing, ion など</td></tr>
<tr><td>4文字</td><td>that, this, with, tion など</td></tr>
<tr><td>5文字以上</td><td>特定の単語の繰り返し</td></tr>
</table>
<h3>活用方法</h3>
<ol>
<li>最も長く頻出するパターンに注目</li>
<li>位置(文頭、文末など)を確認</li>
<li>the, and などの候補を当てはめてみる</li>
</ol>
`
},
wordHints: {
title: '英語の単語パターン',
content: `
<h3>頻出単語ベスト30</h3>
<p style="font-size: 0.85rem;">the, of, and, to, a, in, that, is, I, it, for, as, with, his, he, be, not, by, but, have, you, are, on, or, her, had, at, from, which</p>
<h3>1文字単語</h3>
<p><strong>a</strong> または <strong>I</strong>(常に大文字)のみ</p>
<h3>先頭文字別の頻出単語</h3>
<table class="ref-table">
<tr><th>先頭</th><th>2文字</th><th>3文字</th><th>4文字</th></tr>
<tr><td>a</td><td>as, at, an, am</td><td>and, are, all, any</td><td>-</td></tr>
<tr><td>b</td><td>be, by</td><td>but</td><td>been</td></tr>
<tr><td>c</td><td>-</td><td>can</td><td>come</td></tr>
<tr><td>f</td><td>-</td><td>for</td><td>from</td></tr>
<tr><td>g</td><td>go</td><td>get</td><td>good</td></tr>
<tr><td>h</td><td>he</td><td>had, her, has, him</td><td>have, here</td></tr>
<tr><td>i</td><td>in, it, is, if</td><td>-</td><td>-</td></tr>
<tr><td>n</td><td>no</td><td>not</td><td>-</td></tr>
<tr><td>o</td><td>of, or, on</td><td>one, our, out</td><td>-</td></tr>
<tr><td>t</td><td>to</td><td>the</td><td>that, this, they, time</td></tr>
<tr><td>w</td><td>we</td><td>was</td><td>with, want</td></tr>
<tr><td>y</td><td>-</td><td>you</td><td>your</td></tr>
</table>
<h3>語末パターン</h3>
<table class="ref-table">
<tr><td>-ing</td><td>現在分詞、動名詞</td><td>-tion</td><td>名詞化</td></tr>
<tr><td>-ed</td><td>過去形、過去分詞</td><td>-ly</td><td>副詞</td></tr>
<tr><td>-er</td><td>比較級、行為者</td><td>-est</td><td>最上級</td></tr>
</table>
`
},
frequency: {
title: '英語の文字頻度',
content: `
<h3>各アルファベットの出現確率</h3>
<table class="ref-table">
<tr><th>文字</th><th>確率</th><th>文字</th><th>確率</th><th>文字</th><th>確率</th></tr>
<tr><td><strong>A</strong></td><td>8.2%</td><td>J</td><td>0.2%</td><td>S</td><td>6.3%</td></tr>
<tr><td>B</td><td>1.5%</td><td>K</td><td>0.8%</td><td><strong>T</strong></td><td>9.1%</td></tr>
<tr><td>C</td><td>2.8%</td><td>L</td><td>4.0%</td><td>U</td><td>2.8%</td></tr>
<tr><td>D</td><td>4.3%</td><td>M</td><td>2.4%</td><td>V</td><td>1.0%</td></tr>
<tr><td><strong>E</strong></td><td>12.7%</td><td>N</td><td>6.7%</td><td>W</td><td>2.3%</td></tr>
<tr><td>F</td><td>2.2%</td><td><strong>O</strong></td><td>7.5%</td><td>X</td><td>0.1%</td></tr>
<tr><td>G</td><td>2.0%</td><td>P</td><td>1.9%</td><td>Y</td><td>2.0%</td></tr>
<tr><td>H</td><td>6.1%</td><td>Q</td><td>0.1%</td><td>Z</td><td>0.1%</td></tr>
<tr><td><strong>I</strong></td><td>7.0%</td><td>R</td><td>6.0%</td><td></td><td></td></tr>
</table>
<h3>頻度によるグループ分類</h3>
<table class="ref-table">
<tr><th>グループ</th><th>文字</th><th>確率</th><th>備考</th></tr>
<tr><td>最頻度</td><td>E</td><td>12%</td><td>解読で積極的に利用</td></tr>
<tr><td>高頻度</td><td>T, A, O, I, N, S, H, R</td><td>6〜9%</td><td>よく現れる</td></tr>
<tr><td>中頻度</td><td>D, L, U, C, M</td><td>3〜4%</td><td></td></tr>
<tr><td>低頻度</td><td>P, F, Y, W, G, B, V</td><td>1.5〜2.8%</td><td></td></tr>
<tr><td>稀頻度</td><td>J, K, Q, X, Z</td><td>1%以下</td><td>100文字でも登場しないことがある</td></tr>
</table>
<h3>暗記法: ETAOIN SHRDLU</h3>
<p>最も頻出する12文字を覚えるためのニーモニック。リノタイプ機のキーボード配列に由来。</p>
`
},
ngrams: {
title: 'N-gram(連字)頻度表',
content: `
<h3>頻出2-gram(バイグラム)ベスト25</h3>
<p style="font-size: 0.9rem;"><strong>th, he, in, er, an, re, nd, at, on, nt, ha, es, st, en, ed, to, it, ou, ea, hi, is, or, ti, as, te</strong></p>
<h3>頻出3-gram(トライグラム)ベスト15</h3>
<p style="font-size: 0.9rem;"><strong>the, and, tha, ent, ing, ion, tio, for, nde, has, nce, edt, tis, oft, sth</strong></p>
<h3>頻出4-gram(テトラグラム)ベスト10</h3>
<p style="font-size: 0.9rem;"><strong>that, ther, with, tion, here, ould, ight, have, hich, whic</strong></p>
<p style="font-size: 0.85rem; color: #666; margin-top: 0.25rem;">
その他: ould, atio, ight, ment, ough, ance, ence, ever, ings, ness
</p>
<h3>先頭文字別の連字・3連字</h3>
<table class="ref-table">
<tr><th>先頭</th><th>2-gram</th><th>3-gram</th></tr>
<tr><td>a</td><td>an, at, ar, al</td><td>and</td></tr>
<tr><td>e</td><td>er, es, ea, en, ed, et</td><td>ent, ere, eth</td></tr>
<tr><td>h</td><td>he, ha</td><td>has, her</td></tr>
<tr><td>i</td><td>in, is, it</td><td>ion, ing</td></tr>
<tr><td>n</td><td>nd, nt, ng</td><td>nde, nce</td></tr>
<tr><td>o</td><td>on, or, ou, of</td><td>oft</td></tr>
<tr><td>r</td><td>re, ri</td><td>-</td></tr>
<tr><td>s</td><td>st, se</td><td>sth</td></tr>
<tr><td>t</td><td>th, ti, to, te</td><td>the, tha, tio, tis</td></tr>
</table>
<h3>活用のポイント</h3>
<ul>
<li>THは最も頻出する2-gram</li>
<li>THEは最も頻出する3-gram</li>
<li>THATは最も頻出する4-gram</li>
<li>TIONは名詞の語尾によく現れる4-gram</li>
<li>暗号文の頻出N-gramと比較して推測</li>
</ul>
`
},
pattern: {
title: 'パターン語と非パターン語',
content: `
<h3>パターン語とは</h3>
<p>同じ文字が出てくるパターンのある語。同じ数字は同じ文字を意味します。</p>
<h3>高頻出のパターン語</h3>
<table class="ref-table">
<tr><th>パターン</th><th>パターン語の例</th></tr>
<tr><td>121</td><td>did, eye</td></tr>
<tr><td>122</td><td>add, all, bee, egg, off, see, too</td></tr>
<tr><td>1213</td><td>away, even, ever, nine, none</td></tr>
<tr><td>1221</td><td>noon</td></tr>
<tr><td>1223</td><td>been, book, cook, cool, deep, door, feed, feel, feet, food</td></tr>
<tr><td>1233</td><td>ball, bell, bill, call, fill, free, full, hall, hill, knee, less</td></tr>
<tr><td>12134</td><td>enemy, every, paper, usual</td></tr>
<tr><td>12234</td><td>allow, apple, offer</td></tr>
<tr><td>12314</td><td>catch, clock, enter, taste, truth</td></tr>
<tr><td>12334</td><td>brook, carry, green, happy, hurry, sleep</td></tr>
</table>
<h3>高頻出の非パターン語(すべて異なる文字)</h3>
<table class="ref-table">
<tr><th>文字数</th><th>非パターン語</th></tr>
<tr><td>2文字</td><td>an, as, at, be, by, do, go, he, if, in</td></tr>
<tr><td>3文字</td><td>bow, car, day, ear, far, get, has, its, joy, led</td></tr>
<tr><td>4文字</td><td>able, boat, cent, deal, each, fact, gain, have, into, join</td></tr>
</table>
<h3>高頻出の二重文字ベスト10</h3>
<p><strong>ll, tt, ss, ee, pp, oo, rr, ff, cc, dd</strong></p>
<h3>一般的な反転字(可逆ペア)</h3>
<p style="font-size: 0.9rem;">er-re, es-se, an-na, ti-it, on-no, in-ni, en-ne, at-ta, te-et, or-ro, to-ot, ar-ra, st-ts, is-si, ed-de, of-fo</p>
`
},
kasiski: {
title: 'Kasiskiテストの参考情報',
content: `
<h3>Kasiskiテストとは</h3>
<p>多表式暗号(特にヴィジュネル暗号)の鍵長を推定する手法。繰り返しパターン間の距離の最大公約数から鍵長を推測します。</p>
<h3>手順</h3>
<ol>
<li>暗号文中の繰り返しパターンを見つける</li>
<li>各パターン間の距離を計算</li>
<li>距離の因数を分析</li>
<li>最も多く出現する因数が推定鍵長</li>
</ol>
<h3>注意点</h3>
<ul>
<li>短い暗号文では精度が低下</li>
<li>偶然の一致による誤検出の可能性</li>
<li>複数の候補がある場合は各候補でIC分析を実施</li>
</ul>
<h3>IC値との併用</h3>
<p>Kasiskiで鍵長候補を絞り、その鍵長で暗号文を分割してIC値を計算。IC ≈ 0.067 なら正しい鍵長の可能性が高い。</p>
<h3>単一換字式との区別</h3>
<table class="ref-table">
<tr><th>特徴</th><th>単一換字式</th><th>多表式</th></tr>
<tr><td>IC値</td><td>≈ 0.067</td><td>0.038〜0.055</td></tr>
<tr><td>文字頻度</td><td>英語と類似</td><td>平坦化</td></tr>
<tr><td>Kasiski</td><td>適用外</td><td>有効</td></tr>
</table>
`
}
};
// DOM Elements
const cipherInput = document.getElementById('cipherInput');
const charCount = document.getElementById('charCount');
const btnNormalize = document.getElementById('btnNormalize');
const btnClear = document.getElementById('btnClear');
const btnCopy = document.getElementById('btnCopy');
const btnHelp = document.getElementById('btnHelp');
const btnSave = document.getElementById('btnSave');
const btnLoad = document.getElementById('btnLoad');
const helpModal = document.getElementById('helpModal');
const btnCloseHelp = document.getElementById('btnCloseHelp');
const refModal = document.getElementById('refModal');
const refModalTitle = document.getElementById('refModalTitle');
const refModalBody = document.getElementById('refModalBody');
const btnCloseRef = document.getElementById('btnCloseRef');
const mappingGrid = document.getElementById('mappingGrid');
const btnApplyMapping = document.getElementById('btnApplyMapping');
const btnClearMapping = document.getElementById('btnClearMapping');
const toast = document.getElementById('toast');
const highlightColorSelect = document.getElementById('highlightColor');
// Initialize
document.addEventListener('DOMContentLoaded', init);
function init() {
generateMappingGrid();
setupEventListeners();
setupKeyboardShortcuts();
updateCharCount();
updateViewers();
loadFromStorage();
}
function generateMappingGrid() {
mappingGrid.innerHTML = '';
for (let i = 0; i < 26; i++) {
const letter = String.fromCharCode(65 + i);
const div = document.createElement('div');
div.className = 'mapping-item';
div.innerHTML = `
<span class="mapping-label">${letter}</span>
<input type="text" class="mapping-input" data-letter="${letter}" maxlength="1" autocomplete="off">
`;
mappingGrid.appendChild(div);
}
// Add input handlers
mappingGrid.querySelectorAll('.mapping-input').forEach(input => {
input.addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
if (val && !/^[a-z]$/.test(val)) {
e.target.value = '';
return;
}
e.target.value = val;
e.target.classList.toggle('has-value', val.length > 0);
mapping[e.target.dataset.letter] = val;
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
applyMapping();
}
});
});
}
function setupEventListeners() {
// Input events
cipherInput.addEventListener('input', () => {
updateCharCount();
updateViewers();
});
btnNormalize.addEventListener('click', normalizeCiphertext);
btnClear.addEventListener('click', clearCiphertext);
btnCopy.addEventListener('click', copyCiphertext);
// Save/Load
btnSave.addEventListener('click', saveToStorage);
btnLoad.addEventListener('click', loadFromStorage);
// Mapping
btnApplyMapping.addEventListener('click', applyMapping);
btnClearMapping.addEventListener('click', clearMapping);
// Highlight color
if (highlightColorSelect) {
highlightColorSelect.addEventListener('change', (e) => {
highlightColor = e.target.value;
updateAllViewers();
});
}
// Help modal
btnHelp.addEventListener('click', () => helpModal.classList.remove('hidden'));
btnCloseHelp.addEventListener('click', () => helpModal.classList.add('hidden'));
helpModal.addEventListener('click', (e) => {
if (e.target === helpModal) helpModal.classList.add('hidden');
});
// Reference modal
document.querySelectorAll('.btn-ref').forEach(btn => {
btn.addEventListener('click', () => showReferenceModal(btn.dataset.ref));
});
btnCloseRef.addEventListener('click', () => refModal.classList.add('hidden'));
refModal.addEventListener('click', (e) => {
if (e.target === refModal) refModal.classList.add('hidden');
});
// Phase navigation
document.querySelectorAll('.phase-btn').forEach(btn => {
btn.addEventListener('click', () => switchPhase(parseInt(btn.dataset.phase)));
});
// Tab navigation
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
// Analysis buttons
document.querySelectorAll('.btn-run').forEach(btn => {
btn.addEventListener('click', () => runAnalysis(btn.dataset.analysis));
});
// N-gram mode switch
document.querySelectorAll('[data-ngram]').forEach(btn => {
btn.addEventListener('click', () => {
ngramMode = parseInt(btn.dataset.ngram);
document.querySelectorAll('[data-ngram]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
});
});
// Viewer navigation buttons
setupViewerNavigation('repeated');
setupViewerNavigation('ngrams');
setupViewerNavigation('pattern');
setupViewerNavigation('spacing');
setupViewerNavigation('wordHints');
}
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ignore if typing in input/textarea
if (e.target.matches('input, textarea')) {
if (e.key === 'Escape') {
e.target.blur();
}
return;
}
// Phase switching: 1-4
if (/^[1-4]$/.test(e.key) && !e.ctrlKey && !e.metaKey) {
switchPhase(parseInt(e.key));
return;
}
// Help: ?
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
e.preventDefault();
helpModal.classList.toggle('hidden');
return;
}
// Escape: close modals
if (e.key === 'Escape') {
helpModal.classList.add('hidden');
refModal.classList.add('hidden');
return;
}
// Ctrl+S: Save
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
saveToStorage();
return;
}
// Ctrl+M: Apply mapping
if ((e.ctrlKey || e.metaKey) && e.key === 'm') {
e.preventDefault();
applyMapping();
return;
}
// Ctrl+Enter: Run current analysis
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
runAnalysis(currentTab);
return;
}
});
}
function setupViewerNavigation(tabName) {
const btnPrev = document.getElementById(`btnPrev${capitalize(tabName)}`);
const btnNext = document.getElementById(`btnNext${capitalize(tabName)}`);
const btnToggle = document.getElementById(`btnToggle${capitalize(tabName)}`);
if (btnPrev) {
btnPrev.addEventListener('click', () => navigateHighlight(tabName, -1));
}
if (btnNext) {
btnNext.addEventListener('click', () => navigateHighlight(tabName, 1));
}
if (btnToggle) {
btnToggle.addEventListener('click', () => toggleHighlights(tabName));
}
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Toast notification
function showToast(message, type = '') {
toast.textContent = message;
toast.className = 'toast show ' + type;
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// Reference modal
function showReferenceModal(refType) {
const data = REFERENCE_DATA[refType];
if (!data) return;
refModalTitle.textContent = data.title;
refModalBody.innerHTML = data.content;
refModal.classList.remove('hidden');
}
// Storage functions
function saveToStorage() {
const data = {
ciphertext: cipherInput.value,
originalCiphertext: originalCiphertext,
mapping: mapping,
phase: currentPhase,
tab: currentTab
};
try {
localStorage.setItem('cipher-workbench-state', JSON.stringify(data));
showToast('作業状態を保存しました', 'success');
} catch (e) {
showToast('保存に失敗しました', 'error');
}
}
function loadFromStorage() {
try {
const saved = localStorage.getItem('cipher-workbench-state');
if (saved) {
const data = JSON.parse(saved);
if (data.ciphertext) {
cipherInput.value = data.ciphertext;
updateCharCount();
updateViewers();
}
if (data.originalCiphertext) {
originalCiphertext = data.originalCiphertext;
}
if (data.mapping) {
mapping = data.mapping;
Object.entries(mapping).forEach(([letter, value]) => {
const input = mappingGrid.querySelector(`[data-letter="${letter}"]`);
if (input && value) {
input.value = value;
input.classList.add('has-value');
}
});
}
if (data.phase) {
switchPhase(data.phase);
}
if (data.tab) {
switchTab(data.tab);
}
showToast('作業状態を復元しました', 'success');
}
} catch (e) {
// Ignore load errors
}
}
// Mapping functions
function applyMapping() {
let text = cipherInput.value;
let applied = false;
// 初回適用時に元の暗号文を保存(まだ保存されていない場合)
if (!originalCiphertext) {
// 現在のテキストを大文字に変換して保存
originalCiphertext = text.toUpperCase();
}
Object.entries(mapping).forEach(([upper, lower]) => {
if (lower && lower.length === 1) {
const regex = new RegExp(upper, 'g');
if (text.includes(upper)) {
text = text.replace(regex, lower);
applied = true;
}
}
});
if (applied) {
cipherInput.value = text;
updateCharCount();
updateViewers();
showToast('マッピングを適用しました', 'success');
} else {
showToast('適用する文字がありません', '');
}
}
function clearMapping() {
mapping = {};
mappingGrid.querySelectorAll('.mapping-input').forEach(input => {
input.value = '';
input.classList.remove('has-value');
});
// 元の暗号文があれば復元
if (originalCiphertext) {
cipherInput.value = originalCiphertext;
updateCharCount();
updateViewers();
showToast('マッピングをリセットし、元の暗号文に戻しました', 'success');
} else {
showToast('マッピングをリセットしました', '');
}
}
// Input functions
function updateCharCount() {
charCount.textContent = cipherInput.value.length;
}
function normalizeCiphertext() {
let text = cipherInput.value;
text = text.toUpperCase();
text = text.replace(/[^A-Z\s\n]/g, '');
text = text.replace(/[ \t]+/g, ' ');
text = text.replace(/\n+/g, '\n');
text = text.trim();
cipherInput.value = text;
// 正規化後のテキストを元の暗号文として保存
originalCiphertext = text;
updateCharCount();
updateViewers();
showToast('正規化しました', 'success');
}
function clearCiphertext() {
cipherInput.value = '';
updateCharCount();
updateViewers();
document.querySelectorAll('.panel-body').forEach(panel => {
const placeholder = panel.querySelector('.placeholder');
if (!placeholder) {
panel.innerHTML = '<p class="placeholder">「分析実行」ボタンを押して解析を開始してください。</p>';
}
});
document.querySelectorAll('.last-run').forEach(el => {
el.textContent = '未実行';
});
}
function copyCiphertext() {
navigator.clipboard.writeText(cipherInput.value).then(() => {
showToast('クリップボードにコピーしました', 'success');
}).catch(() => {
showToast('コピーに失敗しました', 'error');
});
}
function updateViewers() {
const text = cipherInput.value;
['repeated', 'wordHints', 'ngrams', 'pattern', 'spacing'].forEach(tabName => {
const viewer = document.getElementById(`viewerText-${tabName}`);
if (viewer) {
viewer.innerHTML = formatCiphertext(text, tabName === 'spacing');
}
});
}
function updateAllViewers() {
['repeated', 'wordHints', 'ngrams', 'pattern'].forEach(tabName => {
if (highlightState[tabName]) {
updateViewerWithHighlights(tabName);
}
});
}
function formatCiphertext(text, showSpaces = false) {
return text.split('').map(char => {
if (/[A-Z]/.test(char)) {
return `<span class="upper">${char}</span>`;
} else if (/[a-z]/.test(char)) {
return `<span class="lower">${char}</span>`;
} else if (showSpaces && char === ' ') {
return `<span class="space-marker"> </span>`;
}
return escapeHtml(char);
}).join('');
}
// Navigation functions
function switchPhase(phase) {
currentPhase = phase;
document.querySelectorAll('.phase-btn').forEach(btn => {
btn.classList.toggle('active', parseInt(btn.dataset.phase) === phase);
});
document.querySelectorAll('.tab-group').forEach(group => {
group.classList.toggle('hidden', parseInt(group.dataset.phase) !== phase);
});
const firstTab = document.querySelector(`.tab-group[data-phase="${phase}"] .tab-btn`);
if (firstTab) {
switchTab(firstTab.dataset.tab);
}
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll(`.tab-group[data-phase="${currentPhase}"] .tab-btn`).forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
document.querySelectorAll('.tab-panel').forEach(panel => {
panel.classList.toggle('active', panel.id === `panel-${tab}`);
});
}
// Analysis functions
function runAnalysis(type) {
const text = cipherInput.value;
if (!text.trim()) {
showToast('暗号文を入力してください', 'error');
return;
}
const timestamp = new Date().toLocaleTimeString('ja-JP');
const lastRunEl = document.getElementById(`lastRun-${type}`);
if (lastRunEl) {
lastRunEl.textContent = `最終実行: ${timestamp}`;
}
switch (type) {
case 'statistics':
analyzeStatistics(text);
break;
case 'ic':
analyzeIC(text);
break;
case 'spacing':
analyzeSpacing(text);
break;
case 'repeated':
analyzeRepeated(text);
break;
case 'wordHints':
analyzeWordHints(text);
break;
case 'frequency':
analyzeFrequency(text);
break;
case 'ngrams':
analyzeNgrams(text);
break;
case 'pattern':
analyzePattern(text);
break;
case 'kasiski':
analyzeKasiski(text);
break;
}
}
// Statistics Analysis
function analyzeStatistics(text) {
const stats = {
total: text.length,
uppercase: (text.match(/[A-Z]/g) || []).length,
lowercase: (text.match(/[a-z]/g) || []).length,
spaces: (text.match(/ /g) || []).length,
newlines: (text.match(/\n/g) || []).length,
symbols: (text.match(/[^A-Za-z\s\n]/g) || []).length,
digits: (text.match(/[0-9]/g) || []).length
};
stats.letters = stats.uppercase + stats.lowercase;
stats.words = text.includes(' ') ? text.split(/\s+/).filter(w => w.length > 0).length : 0;
const unresolvedLetters = new Set(text.match(/[A-Z]/g) || []);
const resolvedLetters = new Set(text.match(/[a-z]/g) || []);
const allLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const usedLetters = new Set([...unresolvedLetters, ...[...resolvedLetters].map(c => c.toUpperCase())]);
const missingLetters = allLetters.filter(l => !usedLetters.has(l));
const progress = stats.letters > 0 ? (stats.lowercase / stats.letters * 100).toFixed(1) : 0;
// 1. 文字の出現回数ランキング
const letterFreq = {};
for (const char of text) {
if (/[A-Z]/.test(char)) {
letterFreq[char] = (letterFreq[char] || 0) + 1;
}
}
const freqRanking = Object.entries(letterFreq)
.sort((a, b) => b[1] - a[1])
.map(([letter, count]) => ({ letter, count }));
// 2. 暗号文の特徴判定
const features = [];
features.push(stats.spaces > 0 ? '✓ スペースあり(単語区切り可能)' : '✗ スペースなし');
features.push(stats.digits > 0 ? '✓ 数字を含む' : '✗ 数字なし');
features.push(stats.symbols > 0 ? `✓ 記号を含む (${stats.symbols}個)` : '✗ 記号なし');
if (unresolvedLetters.size === 26) {
features.push('● 全26文字使用');
} else if (unresolvedLetters.size < 10) {
features.push(`● 使用文字が少ない (${unresolvedLetters.size}種) - 短い暗号文または特殊な暗号の可能性`);
}
// 3. マッピング状況グリッド (26文字)
const mappingGridHtml = allLetters.map(letter => {
const isUnresolved = unresolvedLetters.has(letter);
const mappedTo = mapping[letter] || '';
const hasMapped = mappedTo && /^[a-z]$/.test(mappedTo);
let statusClass = 'unused';
let statusLabel = '';
if (hasMapped) {
statusClass = 'resolved';
statusLabel = mappedTo;
} else if (isUnresolved) {
statusClass = 'unresolved';
}
return `<div class="mapping-status-cell ${statusClass}" title="${letter}${hasMapped ? ' → ' + mappedTo : ''}">
<span class="cell-letter">${letter}</span>
${statusLabel ? `<span class="cell-mapped">${escapeHtml(statusLabel)}</span>` : ''}
</div>`;
}).join('');
// 4. 解読済みマッピング一覧
const appliedMappings = Object.entries(mapping)
.filter(([_, v]) => v)
.sort((a, b) => a[0].localeCompare(b[0]));
const mappingListHtml = appliedMappings.length > 0
? appliedMappings.map(([from, to]) => `<span class="applied-mapping-item">${escapeHtml(from)}→${escapeHtml(to)}</span>`).join('')
: '<span class="no-mapping">マッピング未設定</span>';
const resultEl = document.getElementById('result-statistics');
resultEl.innerHTML = `
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value">${stats.total}</div>
<div class="stat-label">総文字数</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.uppercase}</div>
<div class="stat-label">大文字(未解読)</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.lowercase}</div>
<div class="stat-label">小文字(解読済み)</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.spaces}</div>
<div class="stat-label">スペース</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.newlines}</div>
<div class="stat-label">改行</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.words}</div>
<div class="stat-label">単語数</div>
</div>
</div>
<div class="stats-section">
<h3 class="stats-section-title">暗号文の特徴</h3>
<div class="feature-list">
${features.map(f => `<div class="feature-item">${escapeHtml(f)}</div>`).join('')}
</div>
</div>
<div class="stats-section">
<h3 class="stats-section-title">出現頻度ランキング(未解読文字)</h3>
<div class="freq-ranking">
${freqRanking.length > 0
? freqRanking.map((item, idx) => `<span class="freq-rank-item ${idx < 3 ? 'top' : ''}">${escapeHtml(item.letter)}<small>(${item.count})</small></span>`).join('')
: '<span class="no-data">未解読文字なし</span>'}
</div>
</div>
<div class="stats-section">
<h3 class="stats-section-title">マッピング状況</h3>
<div class="mapping-status-legend">
<span><span class="legend-dot unresolved"></span>未解読</span>
<span><span class="legend-dot resolved"></span>解読済み</span>
<span><span class="legend-dot unused"></span>未使用</span>
</div>
<div class="mapping-status-grid">${mappingGridHtml}</div>
</div>
<div class="stats-section">
<h3 class="stats-section-title">適用中のマッピング</h3>
<div class="applied-mappings">${mappingListHtml}</div>
</div>
<div class="stats-section">
<h3 class="stats-section-title">解読進捗</h3>
<div class="progress-chart-container">
<div class="pie-chart" style="background: conic-gradient(
#27ae60 0deg ${stats.lowercase / stats.letters * 360 || 0}deg,
#e74c3c ${stats.lowercase / stats.letters * 360 || 0}deg ${(stats.lowercase + stats.uppercase) / stats.letters * 360 || 0}deg,
#95a5a6 ${(stats.lowercase + stats.uppercase) / stats.letters * 360 || 0}deg 360deg
);">
<div class="pie-center">
<span class="pie-percent">${progress}%</span>
<span class="pie-label">解読済</span>
</div>
</div>
<div class="pie-legend">
<div class="pie-legend-item"><span class="pie-dot resolved"></span>解読済み (${stats.lowercase})</div>
<div class="pie-legend-item"><span class="pie-dot unresolved"></span>未解読 (${stats.uppercase})</div>
<div class="pie-legend-item"><span class="pie-dot other"></span>その他 (${stats.spaces + stats.newlines + stats.symbols})</div>
</div>
</div>
</div>
`;
}
// Index of Coincidence Analysis
function analyzeIC(text) {
const letters = text.replace(/[^A-Za-z]/g, '').toUpperCase();
const n = letters.length;
if (n < 2) {
document.getElementById('result-ic').innerHTML = '<p class="placeholder">IC計算には2文字以上の英字が必要です。</p>';
return;
}
const freq = {};
for (const char of letters) {
freq[char] = (freq[char] || 0) + 1;
}
let sum = 0;
for (const count of Object.values(freq)) {
sum += count * (count - 1);
}
const ic = sum / (n * (n - 1));