【Minecraft】スコアボードの値を暗号化してみた話

## はじめに
### この記事は何？
この記事を読むと、storageにリスト型で格納した任意個数の数値を改ざんされにくい暗号文として出力し、暗号文から数値を復元し、storageへ再格納できるようになります。
1. 任意個数の数値をリスト型で格納
`[0,123,-1]`
2. 暗号文の出力
`B4Cy5J5M`
3. 暗号文から数値に復元
`[0,123,-1]`

### 使いどころは？
データの数値を隠して伝達することができるため、配布マップのデータ引継ぎに使えるかもしれません。

Minecraftの配布マップには、基本的に外部との通信を行う機能がなく、不具合修正や新コンテンツの実装などを、プレイヤーが利用しているデータ上で直接行うことは困難です。
そのため、アップデートを行う際には、新しい配布マップのデータをアップロードし、プレイヤーに再度ダウンロードしてもらう方法が一般的だと思われます。

その際、再ダウンロードしたプレイヤーは配布マップを最初から遊ぶことになりますが、進行状況やアイテム情報を含むデータを何らかしらの方法で持っていくことができればプレイヤーは進行状況を維持してアップデートされた配布マップを遊ぶことができます。

1. 進行状況などの数値化
ゲームの進行状況などのゲーム内データを数値化し、それらをもとに暗号文を生成します。
1. 暗号文の送信
暗号文はテキストチャットなどでプレイヤーに送信されます。ここまでがアップデート前の配布マップで行う操作になります。
1. 暗号文の入力
プレイヤーはアップデートされた配布マップをダウンロードし、新規ゲームとして開始します。ゲーム開始後、暗号文を入力します。
1. 進行状況などの引継ぎ
入力された暗号文は復号され、数値化されます。復号化された数値をもとにゲームの進行状況などを設定します。

このようにして、アップデート前の進捗を引き継いだ状態でゲームを続けることが可能です。
プレイヤーに必要以上のファイル操作を要求せず、引継ぎがゲーム内で完結することがこの方法の特徴です。

### 配布物
この記事に関連するデータパックは[GitHub](https://github.com/Elias-rin/Crypto_DataPack/tree/main)にて配布しています。手元に配布データパックを用意しておくことをお勧めします。

https://github.com/Elias-rin/Crypto_DataPack/tree/main

## 暗号化
### 目指した暗号文
* 同じデータでも複数パターンの暗号文が存在する
例)`[0,123,-1]`の暗号文
`B4Cy5J5M` , `C6E36B9J` , `D8G66G3T` など
* 任意個数の数値が扱える
1個以上の数値であれば何個でもまとめて暗号化できるようにします。
* 数値が同じであっても規則性を見せない
例)`[987,987,987,987]`の暗号文
`BxyFyyMzyG2zC`
* 改ざんされにくい暗号文である
チェックサムを用いて暗号文が改ざんされていないかを確認します。
* scoreboardで扱える範囲の数値で動作する
Minecraftのscoreboardは-2147483648から2147483647の整数を扱うことができ、オーバーフロー時には逆側の最大値に反転します。
* Minecraftのコマンドで扱いやすい暗号文である
暗号文を復号する際には1文字ずつ処理します。複数文字を一度に処理する必要がないような暗号文にします。

### 暗号文の仕様
**使用文字**
暗号文には大文字英字、小文字英字、数字のうち、視認性が悪いものを除いた56文字を使用します。

```
2 3 4 5 6 7 8 9
a b c d e f g h i j k m n p q r s t u v w x y z
A B C D E F G H J K L M N P Q R S T U V W X Y Z
```

これらの文字を3つのグループに分けて使用します。

```
# 数値を32進数で管理
2 3 4 5 6 7 8 9 a b c d e f g h i j k m n p q r s t u v w x y z
# 乱数と数値が正の値である場合のチェックサムを管理
A B C D E F G H J K L M
# 数値が負の値である場合のチェックサムを管理
N P Q R S T U V W X Y Z
```

**暗号化の手順**
1. 1から11の乱数を生成し、文字変換して暗号文に付与する
1. 加算用の乱数を生成した乱数と同値に設定する
1. 加算用の乱数に生成した乱数を加算する
1. データから数値を取り出し、数値に加算用の乱数を加算する
1. チェックサム用の変数に数値を加算する
1. 数値が負の値であれば-1倍する
1. 数値を32進数に変換し、文字変換して暗号文に付与する
1. 数値の正負とチェックサムの情報を終端文字として暗号文に付与する
1. データからすべての数値を取り出すまで3. から8. の処理を繰り返す

暗号文は(乱数)(数値)(終端文字)...(数値)(終端文字)という順で構成されます。

<details><summary>処理プロセス (折りたたみ)</summary>

```:(乱数=1) [0,123,-1]
処理中	処理内容				処理結果			出力
(1)		rand(1~11)			rand_num=1
(1)		rand_sum=rand_num	rand_sum=1
(1)		rand_num文字化		B				B
		rand_sum+=rand_num	rand_sum=2		B
		rand_sum%=12		rand_sum=2		B
0		score=plaintext		score=0			B
0		score+=rand_sum		score=2			B
0		checksum+=score		checksum=2		B
0		checksum%=12		checksum=2		B
0		num=score			num=2			B
0		num%=32				num=2			B
0		score/=32			score=0			B
0		num文字化			4				B4
0		checksum文字化		C				B4C
		rand_sum+=rand_num	rand_sum=3		B4C
		rand_sum%=12		rand_sum=3		B4C
123		score=plaintext		score=123		B4C
123		score+=rand_sum		score=126		B4C
123		checksum+=score		checksum=128	B4C
123		checksum%=12		checksum=8		B4C
123		num=score			num=126			B4C
123		num%=32				num=30			B4C
123		score/=32			score=3			B4C
123		num文字化			y				B4Cy
123		num=score			num=3			B4Cy
123		num%=32				num=3			B4Cy
123		score/=32			score=0			B4Cy
123		num文字化			5				B4Cy5
123		checksum文字化		J				B4Cy5J
		rand_sum+=rand_num	rand_sum=4		B4Cy5J
		rand_sum%=12		rand_sum=4		B4Cy5J
-1		score=plaintext		score=-1		B4Cy5J
-1		score+=rand_sum		score=3			B4Cy5J
-1		checksum+=score		checksum=11		B4Cy5J
-1		checksum%=12		checksum=11		B4Cy5J
-1		num=score			num=3			B4Cy5J
-1		num%=32				num=3			B4Cy5J
-1		score/=32			score=0			B4Cy5J
-1		num文字化			5				B4Cy5J5
-1		checksum文字化		M				B4Cy5J5M
```

</details>

### 実装
**pythonでの実装**
いきなりMinecraftで実装するのは難易度が高そうなので一度pythonで実装します。
[Google Colab](https://colab.google/)などを利用して実行してみると暗号化のイメージが掴めると思います。
なるべくMinecraftに落とし込みやすいように組んでいます。

<details><summary>encryption.py (折りたたみ)</summary>

```python:encryption.py
import random

to_char = ['2','3','4','5','6','7','8','9',
           'a','b','c','d','e','f','g','h',
           'i','j','k','m','n','p','q','r',
           's','t','u','v','w','x','y','z']
to_positive_char = ['A','B','C','D','E','F','G','H','J','K','L','M']
to_negative_char = ['N','P','Q','R','S','T','U','V','W','X','Y','Z']

def overflow(argument_int):
    argument_int = (argument_int + 4294967296) % 4294967296
    if 2147483647 < argument_int:
        argument_int -= 4294967296
    return argument_int

def yes_no():
    while True:
        yes_no_input = input('再実行(y/n)：').lower()
        if yes_no_input in ['y', 'ye', 'yes']:
            return True
        elif yes_no_input in ['n', 'no']:
            return False

while True:
    checksum = 0
    plaintext = list(map(int, input('-2147483648~2147483647の整数を任意個数入力\n区切りはカンマ\n例)0,123,-1\n').split(',')))
    print('plaintext =',plaintext)
    random_num = random.randint(1,11)
    random_sum = random_num
    ciphertext = [to_positive_char[random_num]]
    while True:
        random_sum += random_num
        random_sum %= 12
        plain_score = plaintext[0]
        plaintext.pop(0)
        plain_score = overflow(plain_score + random_sum)
        checksum = overflow(checksum + plain_score)
        checksum %= 12
        if 0 <= plain_score:
            while True:
                plain_num = plain_score % 32
                plain_score //= 32
                ciphertext.append(to_char[plain_num])
                if plain_score == 0:
                    break
            ciphertext.append(to_positive_char[checksum])
        if plain_score <= -1:
            plain_score *= -1
            while True:
                plain_num = plain_score % 32
                plain_score //= 32
                ciphertext.append(to_char[plain_num])
                if plain_score == 0:
                    break
            ciphertext.append(to_negative_char[checksum])
        if not plaintext:
            break
    print('ciphertext =',ciphertext)
    print('暗号文 =',''.join(ciphertext))
    if yes_no() == False:
        break
```

</details>

**Minecraftでの実装**
pythonで組んだプログラムを参考に暗号化functionを作成します。
暗号化に関するfunctionの構成は以下のようになっています。

```
data/
└code/
 └function/
  └encryption/
   ├run.mcfunction
   ├to_char.mcfunction
   ├to_positive_char.mcfunction
   ├to_negative_char.mcfunction
   ├score.mcfunction
   ├score_positive.mcfunction
   ├score_negative.mcfunction
   └num.mcfunction
```

`encryption/run.mcfunction`を実行することで`storage code: plaintext`に設定したデータを暗号化して`storage code: ciphertext`に格納することができます。

<details><summary>encryption/run.mcfunction (折りたたみ)</summary>

```mcfunction:run.mcfunction
#> code:encryption/run
#code: plaintext set value [0,123,-1]

#初期化
data remove storage code: ciphertext
scoreboard objectives add code_encryption dummy
scoreboard players set #-1 code_encryption -1
scoreboard players set #12 code_encryption 12
scoreboard players set #32 code_encryption 32
scoreboard players set #checksum code_encryption 0

#データセット
data modify storage code: encryption.to_char set value ["2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","m","n","p","q","r","s","t","u","v","w","x","y","z"]
data modify storage code: encryption.to_positive_char set value ["A","B","C","D","E","F","G","H","J","K","L","M"]
data modify storage code: encryption.to_negative_char set value ["N","P","Q","R","S","T","U","V","W","X","Y","Z"]

#乱数付与
execute store result score #random_num code_encryption run random value 1..11
scoreboard players operation #random_sum code_encryption = #random_num code_encryption
execute store result storage code: encryption.argument.int int 1 run scoreboard players get #random_num code_encryption
function code:encryption/to_positive_char with storage code: encryption.argument
data modify storage code: ciphertext append from storage code: encryption.return

#数値処理
function code:encryption/score

#終了処理
data remove storage code: plaintext
scoreboard objectives remove code_encryption
data remove storage code: encryption

#表示
tellraw @a [{"text":"> "},{"nbt":"ciphertext","storage":"code:","interpret":true}]
```

</details>

<details><summary>encryption/to_char.mcfunction (折りたたみ)</summary>

```mcfunction:to_char.mcfunction
#> code:encryption/to_char
#code: encryption.argument.int
#code: encryption.return

$data modify storage code: encryption.return set from storage code: encryption.to_char[$(int)]
```

</details>

<details><summary>encryption/to_positive_char.mcfunction (折りたたみ)</summary>

```mcfunction:to_positive_char.mcfunction
#> code:encryption/to_positive_char
#code: encryption.argument.int
#code: encryption.return

$data modify storage code: encryption.return set from storage code: encryption.to_positive_char[$(int)]
```

</details>

<details><summary>encryption/to_negative_char.mcfunction (折りたたみ)</summary>

```mcfunction:to_negative_char.mcfunction
#> code:encryption/to_negative_char
#code: encryption.argument.int
#code: encryption.return

$data modify storage code: encryption.return set from storage code: encryption.to_negative_char[$(int)]
```

</details>

<details><summary>encryption/score.mcfunction (折りたたみ)</summary>

```mcfunction:score.mcfunction
#> code:encryption/score

#数値処理
scoreboard players operation #random_sum code_encryption += #random_num code_encryption
scoreboard players operation #random_sum code_encryption %= #12 code_encryption
execute store result score #plain_score code_encryption run data get storage code: plaintext[0]
data remove storage code: plaintext[0]
scoreboard players operation #plain_score code_encryption += #random_sum code_encryption
scoreboard players operation #checksum code_encryption += #plain_score code_encryption
scoreboard players operation #checksum code_encryption %= #12 code_encryption
execute if score #plain_score code_encryption matches 0.. run function code:encryption/score_positive
execute if score #plain_score code_encryption matches ..-1 run function code:encryption/score_negative

#break条件
execute if data storage code: {plaintext:[]} run return 0

#再帰
function code:encryption/score
```

</details>

<details><summary>encryption/score_positive.mcfunction (折りたたみ)</summary>

```mcfunction:score_positive.mcfunction
#> code:encryption/score_positive

#数値変換
function code:encryption/num

#終端文字付与
execute store result storage code: encryption.argument.int int 1 run scoreboard players get #checksum code_encryption
function code:encryption/to_positive_char with storage code: encryption.argument
data modify storage code: ciphertext append from storage code: encryption.return
```

</details>

<details><summary>encryption/score_negative.mcfunction (折りたたみ)</summary>

```mcfunction:score_negative.mcfunction
#> code:encryption/score_negative

#正負反転
scoreboard players operation #plain_score code_encryption *= #-1 code_encryption

#数値変換
function code:encryption/num

#終端文字付与
execute store result storage code: encryption.argument.int int 1 run scoreboard players get #checksum code_encryption
function code:encryption/to_negative_char with storage code: encryption.argument
data modify storage code: ciphertext append from storage code: encryption.return
```

</details>

<details><summary>encryption/num.mcfunction (折りたたみ)</summary>

```mcfunction:num.mcfunction
#> code:encryption/num

#32進数変換
scoreboard players operation #plain_num code_encryption = #plain_score code_encryption
scoreboard players operation #plain_num code_encryption %= #32 code_encryption
scoreboard players operation #plain_score code_encryption /= #32 code_encryption
execute store result storage code: encryption.argument.int int 1 run scoreboard players get #plain_num code_encryption
function code:encryption/to_char with storage code: encryption.argument
data modify storage code: ciphertext append from storage code: encryption.return

#break条件
execute if score #plain_score code_encryption matches 0 run return 0

#再帰
function code:encryption/num
```

</details>

**tags登録**
手動実行するfunctionにアクセスしやすいよう、`encryption/run.mcfunction`をtagsに登録します。

```
data/
└code/
 └tags/
  └function/
   └encryption.json
```

<details><summary>encryption.json (折りたたみ)</summary>

```json:encryption.json
{
  "values": [
    "code:encryption/run"
  ]
}
```

</details>

### 実行
暗号化の実行にはデータの設定が必要です。以下のコマンドを実行し、`storage code: plaintext`にデータを設定します。

```mcfunction:[0,123,-1]を設定したい場合
data modify storage code: plaintext set value [0,123,-1]
```

![スクリーンショット 2024-11-25 212511.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/2540c038-f5b2-57de-b831-8e4a02af9574.png)

設定完了後に暗号化を実行します。

```mcfunction:暗号化の実行
function #code:encryption
```

![スクリーンショット 2024-11-25 212546.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/6767f5d1-fb4d-88e4-e546-c523e93f4963.png)
![スクリーンショット 2024-11-25 212626.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/4ab0d6bd-8759-26ea-c8ee-f88ee19b604d.png)

実行すると暗号化された文字列が表示されます。
実行後は`storage code: ciphertext`に文字列が1文字ずつリスト型で格納されます。

![スクリーンショット 2024-11-25 214131.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/cf2360bf-652c-9c3e-2314-0cc455d00c76.png)
![スクリーンショット 2024-11-25 214132.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/d91ac26e-52a8-891e-219e-cf9f3c1392a4.png)

## 復号化
### 実装
暗号化の手順をもとに復号化を行います。
プレイヤーが正しい暗号文を入力するとは限らないので正しい暗号文であるかを確認する必要があります。

**pythonでの実装**
暗号化と同じように一度pythonで実装します。
コメント文で示している部分の処理で正しい暗号文であるかを判断しています。

<details><summary>decryption.py (折りたたみ)</summary>

```python:decryption.py
def to_random(argument_char):
    return_int = {
        'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'J': 8, 'K': 9, 'L': 10, 'M': 11
    }
    return return_int.get(argument_char, -1)

def to_number(argument_char):
    return_int = {
        '2':  0, '3':  1, '4':  2, '5':  3, '6':  4, '7':  5, '8':  6, '9':  7,
        'a':  8, 'b':  9, 'c': 10, 'd': 11, 'e': 12, 'f': 13, 'g': 14, 'h': 15,
        'i': 16, 'j': 17, 'k': 18, 'm': 19, 'n': 20, 'p': 21, 'q': 22, 'r': 23,
        's': 24, 't': 25, 'u': 26, 'v': 27, 'w': 28, 'x': 29, 'y': 30, 'z': 31,
        'A': 32, 'B': 33, 'C': 34, 'D': 35, 'E': 36, 'F': 37, 'G': 38, 'H': 39,
        'J': 40, 'K': 41, 'L': 42, 'M': 43, 'N': 44, 'P': 45, 'Q': 46, 'R': 47,
        'S': 48, 'T': 49, 'U': 50, 'V': 51, 'W': 52, 'X': 53, 'Y': 54, 'Z': 55
    }
    return return_int.get(argument_char, -1)

def overflow(argument_int):
    argument_int = (argument_int + 4294967296) % 4294967296
    if 2147483647 < argument_int:
        argument_int -= 4294967296
    return argument_int

def yes_no():
    while True:
        yes_no_input = input('再実行(y/n)：').lower()
        if yes_no_input in ['y', 'ye', 'yes']:
            return True
        elif yes_no_input in ['n', 'no']:
            return False

while True:
    dechiphertext = []
    dechipher_score = 0
    dechipher_scale = 1
    checksum = 0
    flag_error = False
    flag_zero = False
    ciphertext = list(input('暗号文を入力\n例)B4Cy5J5M\n'))
    print('ciphertext =',ciphertext)
    if not ciphertext:
        flag_error = True
        print('1文字目不足')    #暗号文は3文字以上でなければならない
    random_num = to_random(ciphertext[0])
    ciphertext.pop(0)
    if random_num == -1:
        flag_error = True
        print('不使用文字') #暗号文で使用されない文字が含まれてはならない
    if not ciphertext:
        flag_error = True
        print('2文字目不足')    #暗号文は3文字以上でなければならない
    random_sum = random_num
    while True:
        if flag_error == True:
            break
        dechipher_num = to_number(ciphertext[0])
        ciphertext.pop(0)
        if dechipher_num == -1:
            flag_error = True
            print('不使用文字') #暗号文で使用されない文字が含まれてはならない
        if 0 <= dechipher_num and dechipher_num <= 31:
            flag_zero = False
            if dechipher_num == 0 and 1 < dechipher_scale:
                flag_zero = True
            if dechipher_scale == 1073741824 and 3 <= dechipher_num:
                flag_error = True
                print('(32^6)*3以上の数値') #-2147483648~2147483647以外の数値は扱わない
            if dechipher_scale < 1:
                flag_error = True
                print('7連続数値文字')  #-2147483648~2147483647以外の数値は扱わない
            if not ciphertext:
                flag_error = True
                print('終端文字不足')   #数値文字の後には終端文字が必要
            dechipher_num *= dechipher_scale
            dechipher_score = overflow(dechipher_score + dechipher_num)
            if -2147483648 < dechipher_score and dechipher_score < 0:
                flag_error = True
                print('((32^6)*3)+1以上の数値') #-2147483648~2147483647以外の数値は扱わない
            dechipher_scale = overflow(dechipher_scale * 32)
        elif 32 <= dechipher_num and dechipher_num <= 43:
            if flag_zero == True:
                flag_error = True
                print('数値0終了')  #01など、32進数の先頭に0は存在してはならない
            if dechipher_scale == 1:
                flag_error = True
                print('連続終端文字')   #終端文字の前には数値文字が必要
            dechipher_num -= 32
            checksum = overflow(checksum + dechipher_score)
            checksum %= 12
            if dechipher_num != checksum:
                flag_error = True
                print('チェックサム不一致') #改ざんの検知
            random_sum += random_num
            random_sum %= 12
            dechipher_score = overflow(dechipher_score - random_sum)
            dechiphertext.append(dechipher_score)
            dechipher_score = 0
            dechipher_scale = 1
        elif 44 <= dechipher_num and dechipher_num <= 55:
            if flag_zero == True:
                flag_error = True
                print('数値0終了')  #01など、32進数の先頭に0は存在してはならない
            if dechipher_scale == 1:
                flag_error = True
                print('連続終端文字')   #終端文字の前には数値文字が必要
            if dechipher_score == 0:
                flag_error = True
                print('負終端数値0')    #絶対値0の場合に負の値であることを示す終端文字が来てはならない
            dechipher_num -= 44
            dechipher_score *= -1
            checksum = overflow(checksum + dechipher_score)
            checksum %= 12
            if dechipher_num != checksum:
                flag_error = True
                print('チェックサム不一致') #改ざんの検知
            random_sum += random_num
            random_sum %= 12
            dechipher_score = overflow(dechipher_score - random_sum)
            dechiphertext.append(dechipher_score)
            dechipher_score = 0
            dechipher_scale = 1
        if not ciphertext:
            break
    if flag_error == True:
        print('復号不可')
        dechiphertext = []
    print('deciphertext =',dechiphertext)
    if yes_no() == False:
        break
```

</details>

**Minecraftでの実装**
pythonで組んだプログラムを参考に復号化functionを作成します。
復号化に関するfunctionの構成は以下のようになっています。

```
data/
└code/
 └function/
  └decryption/
   ├run.mcfunction
   ├to_random.mcfunction
   ├to_number.mcfunction
   ├num.mcfunction
   ├score.mcfunction
   ├num_positive.mcfunction
   └num_negative.mcfunction
```

`decryption/run.mcfunction`を実行することで`storage code: ciphertext`に設定したデータを復号化して`storage code: deciphertext`に格納することができます。

<details><summary>decryption/run.mcfunction (折りたたみ)</summary>

```mcfunction:run.mcfunction
#> code:decryption/run
#code: ciphertext set value ["B","4","C","y","5","J","5","M"]

#初期化
data remove storage code: deciphertext
scoreboard objectives add code_decryption dummy
scoreboard players set #-1 code_decryption -1
scoreboard players set #12 code_decryption 12
scoreboard players set #32 code_decryption 32
scoreboard players set #44 code_decryption 44
scoreboard players set #dechipher_score code_decryption 0
scoreboard players set #dechipher_scale code_decryption 1
scoreboard players set #checksum code_decryption 0
scoreboard players set #flag_error code_decryption 0
scoreboard players set #flag_zero code_decryption 0

#データ未設定エラー
execute unless data storage code: ciphertext run scoreboard players set #flag_error code_decryption 1
#1文字目不足エラー
execute if data storage code: {ciphertext:[]} run scoreboard players set #flag_error code_decryption 1

#乱数抽出
data modify storage code: decryption.argument set from storage code: ciphertext[0]
data remove storage code: ciphertext[0]
execute store result score #random_num code_decryption run function code:decryption/to_random
scoreboard players operation #random_sum code_decryption = #random_num code_decryption

#2文字目不足エラー
execute if data storage code: {ciphertext:[]} run scoreboard players set #flag_error code_decryption 1

#2文字目以降処理
execute if score #flag_error code_decryption matches 0 run function code:decryption/num

#終了処理
data remove storage code: ciphertext
execute if score #flag_error code_decryption matches 1 run data remove storage code: deciphertext
scoreboard objectives remove code_decryption
data remove storage code: decryption

#表示
execute unless data storage code: deciphertext run tellraw @a [{"text":"復号不可"}]
execute if data storage code: deciphertext run tellraw @a [{"text":"> "},{"nbt":"deciphertext","storage":"code:"}]
```

</details>

<details><summary>decryption/to_random.mcfunction (折りたたみ)</summary>

```mcfunction:to_random.mcfunction
#> code:decryption/to_random

execute if data storage code: {decryption:{argument:"B"}} run return 1
execute if data storage code: {decryption:{argument:"C"}} run return 2
execute if data storage code: {decryption:{argument:"D"}} run return 3
execute if data storage code: {decryption:{argument:"E"}} run return 4
execute if data storage code: {decryption:{argument:"F"}} run return 5
execute if data storage code: {decryption:{argument:"G"}} run return 6
execute if data storage code: {decryption:{argument:"H"}} run return 7
execute if data storage code: {decryption:{argument:"J"}} run return 8
execute if data storage code: {decryption:{argument:"K"}} run return 9
execute if data storage code: {decryption:{argument:"L"}} run return 10
execute if data storage code: {decryption:{argument:"M"}} run return 11
scoreboard players set #flag_error code_decryption 1
return -1
```

</details>

<details><summary>decryption/to_number.mcfunction (折りたたみ)</summary>

```mcfunction:to_number.mcfunction
#> code:decryption/to_number

execute if data storage code: {decryption:{argument:"2"}} run return 0
execute if data storage code: {decryption:{argument:"3"}} run return 1
execute if data storage code: {decryption:{argument:"4"}} run return 2
execute if data storage code: {decryption:{argument:"5"}} run return 3
execute if data storage code: {decryption:{argument:"6"}} run return 4
execute if data storage code: {decryption:{argument:"7"}} run return 5
execute if data storage code: {decryption:{argument:"8"}} run return 6
execute if data storage code: {decryption:{argument:"9"}} run return 7
execute if data storage code: {decryption:{argument:"a"}} run return 8
execute if data storage code: {decryption:{argument:"b"}} run return 9
execute if data storage code: {decryption:{argument:"c"}} run return 10
execute if data storage code: {decryption:{argument:"d"}} run return 11
execute if data storage code: {decryption:{argument:"e"}} run return 12
execute if data storage code: {decryption:{argument:"f"}} run return 13
execute if data storage code: {decryption:{argument:"g"}} run return 14
execute if data storage code: {decryption:{argument:"h"}} run return 15
execute if data storage code: {decryption:{argument:"i"}} run return 16
execute if data storage code: {decryption:{argument:"j"}} run return 17
execute if data storage code: {decryption:{argument:"k"}} run return 18
execute if data storage code: {decryption:{argument:"m"}} run return 19
execute if data storage code: {decryption:{argument:"n"}} run return 20
execute if data storage code: {decryption:{argument:"p"}} run return 21
execute if data storage code: {decryption:{argument:"q"}} run return 22
execute if data storage code: {decryption:{argument:"r"}} run return 23
execute if data storage code: {decryption:{argument:"s"}} run return 24
execute if data storage code: {decryption:{argument:"t"}} run return 25
execute if data storage code: {decryption:{argument:"u"}} run return 26
execute if data storage code: {decryption:{argument:"v"}} run return 27
execute if data storage code: {decryption:{argument:"w"}} run return 28
execute if data storage code: {decryption:{argument:"x"}} run return 29
execute if data storage code: {decryption:{argument:"y"}} run return 30
execute if data storage code: {decryption:{argument:"z"}} run return 31
execute if data storage code: {decryption:{argument:"A"}} run return 32
execute if data storage code: {decryption:{argument:"B"}} run return 33
execute if data storage code: {decryption:{argument:"C"}} run return 34
execute if data storage code: {decryption:{argument:"D"}} run return 35
execute if data storage code: {decryption:{argument:"E"}} run return 36
execute if data storage code: {decryption:{argument:"F"}} run return 37
execute if data storage code: {decryption:{argument:"G"}} run return 38
execute if data storage code: {decryption:{argument:"H"}} run return 39
execute if data storage code: {decryption:{argument:"J"}} run return 40
execute if data storage code: {decryption:{argument:"K"}} run return 41
execute if data storage code: {decryption:{argument:"L"}} run return 42
execute if data storage code: {decryption:{argument:"M"}} run return 43
execute if data storage code: {decryption:{argument:"N"}} run return 44
execute if data storage code: {decryption:{argument:"P"}} run return 45
execute if data storage code: {decryption:{argument:"Q"}} run return 46
execute if data storage code: {decryption:{argument:"R"}} run return 47
execute if data storage code: {decryption:{argument:"S"}} run return 48
execute if data storage code: {decryption:{argument:"T"}} run return 49
execute if data storage code: {decryption:{argument:"U"}} run return 50
execute if data storage code: {decryption:{argument:"V"}} run return 51
execute if data storage code: {decryption:{argument:"W"}} run return 52
execute if data storage code: {decryption:{argument:"X"}} run return 53
execute if data storage code: {decryption:{argument:"Y"}} run return 54
execute if data storage code: {decryption:{argument:"Z"}} run return 55
scoreboard players set #flag_error code_decryption 1
return -1
```

</details>

<details><summary>decryption/num.mcfunction (折りたたみ)</summary>

```mcfunction:num.mcfunction
#> code:decryption/num

#数字抽出
data modify storage code: decryption.argument set from storage code: ciphertext[0]
data remove storage code: ciphertext[0]
execute store result score #dechipher_num code_decryption run function code:decryption/to_number

#数値
execute if score #dechipher_num code_decryption matches 0..31 run function code:decryption/score
#正終端
execute if score #dechipher_num code_decryption matches 32..43 run function code:decryption/num_positive
#負終端
execute if score #dechipher_num code_decryption matches 44..55 run function code:decryption/num_negative

#break条件
execute if data storage code: {ciphertext:[]} run return 0

#再帰
execute if score #flag_error code_decryption matches 0 run function code:decryption/num
```

</details>

<details><summary>decryption/score.mcfunction (折りたたみ)</summary>

```mcfunction:score.mcfunction
#> code:decryption/score

#0フラグ処理
scoreboard players set #flag_zero code_decryption 0
execute if score #dechipher_num code_decryption matches 0 if score #dechipher_scale code_decryption matches 3.. run scoreboard players set #flag_zero code_decryption 1

#範囲外数値エラー
execute if score #dechipher_scale code_decryption matches 1073741824 if score #dechipher_num code_decryption matches 3.. run scoreboard players set #flag_error code_decryption 1
execute if score #dechipher_scale code_decryption matches ..0 run scoreboard players set #flag_error code_decryption 1

#終端文字不足エラー
execute if data storage code: {ciphertext:[]} run scoreboard players set #flag_error code_decryption 1

#10進数変換
scoreboard players operation #operation_num code_decryption = #dechipher_num code_decryption
scoreboard players operation #operation_num code_decryption *= #dechipher_scale code_decryption
scoreboard players operation #dechipher_score code_decryption += #operation_num code_decryption

#範囲外数値エラー
execute if score #dechipher_score code_decryption matches -2147483648..-1 run scoreboard players set #flag_error code_decryption 1

#scale処理
scoreboard players operation #dechipher_scale code_decryption *= #32 code_decryption
```

</details>

<details><summary>decryption/num_positive.mcfunction (折りたたみ)</summary>

```mcfunction:num_positive.mcfunction
#> code:decryption/num_positive

#0フラグエラー
execute if score #flag_zero code_decryption matches 1 run scoreboard players set #flag_error code_decryption 1

#連続終端文字エラー
execute if score #dechipher_scale code_decryption matches 1 run scoreboard players set #flag_error code_decryption 1

#数字処理
scoreboard players operation #operation_num code_decryption = #dechipher_num code_decryption
scoreboard players operation #operation_num code_decryption -= #32 code_decryption

#チェックサム処理
scoreboard players operation #checksum code_decryption += #dechipher_score code_decryption
scoreboard players operation #checksum code_decryption %= #12 code_decryption

#チェックサムエラー
execute unless score #operation_num code_decryption = #checksum code_decryption run scoreboard players set #flag_error code_decryption 1

#数値処理
scoreboard players operation #random_sum code_decryption += #random_num code_decryption
scoreboard players operation #random_sum code_decryption %= #12 code_decryption
scoreboard players operation #dechipher_score code_decryption -= #random_sum code_decryption
execute store result storage code: decryption.append int 1 run scoreboard players get #dechipher_score code_decryption
data modify storage code: deciphertext append from storage code: decryption.append

#初期化
scoreboard players set #dechipher_score code_decryption 0
scoreboard players set #dechipher_scale code_decryption 1
```

</details>

<details><summary>decryption/num_negative.mcfunction (折りたたみ)</summary>

```mcfunction:num_negative.mcfunction
#> code:decryption/num_negative

#0フラグエラー
execute if score #flag_zero code_decryption matches 1 run scoreboard players set #flag_error code_decryption 1

#連続終端文字エラー
execute if score #dechipher_scale code_decryption matches 1 run scoreboard players set #flag_error code_decryption 1

#負終端数値0エラー
execute if score #dechipher_score code_decryption matches 0 run scoreboard players set #flag_error code_decryption 1

#数字処理
scoreboard players operation #operation_num code_decryption = #dechipher_num code_decryption
scoreboard players operation #operation_num code_decryption -= #44 code_decryption

#正負反転
scoreboard players operation #dechipher_score code_decryption *= #-1 code_decryption

#チェックサム処理
scoreboard players operation #checksum code_decryption += #dechipher_score code_decryption
scoreboard players operation #checksum code_decryption %= #12 code_decryption

#チェックサムエラー
execute unless score #operation_num code_decryption = #checksum code_decryption run scoreboard players set #flag_error code_decryption 1

#数値処理
scoreboard players operation #random_sum code_decryption += #random_num code_decryption
scoreboard players operation #random_sum code_decryption %= #12 code_decryption
scoreboard players operation #dechipher_score code_decryption -= #random_sum code_decryption
execute store result storage code: decryption.append int 1 run scoreboard players get #dechipher_score code_decryption
data modify storage code: deciphertext append from storage code: decryption.append

#初期化
scoreboard players set #dechipher_score code_decryption 0
scoreboard players set #dechipher_scale code_decryption 1
```

</details>

**tags登録**
手動実行するfunctionにアクセスしやすいよう、`decryption/run.mcfunction`をtagsに登録します。

```
data/
└code/
 └tags/
  └function/
   └decryption.json
```

<details><summary>decryption.json (折りたたみ)</summary>

```json:decryption.json
{
  "values": [
    "code:decryption/run"
  ]
}
```

</details>

### 実行
復号化の実行にはデータの設定が必要です。暗号化を実行済みの場合はすでにデータが設定されているため必要ありませんが、未実行の場合は以下のコマンドを実行し、`storage code: ciphertext`にデータを設定します。

```mcfunction:["B","4","C","y","5","J","5","M"]を設定したい場合
data modify storage code: ciphertext set value ["B","4","C","y","5","J","5","M"]
```

![スクリーンショット 2024-11-25 221831.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/5a191da3-7ca3-a918-1816-0188ded5835c.png)

設定完了後に復号化を実行します。

```mcfunction:復号化の実行
function #code:decryption
```

![スクリーンショット 2024-11-25 222149.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/afdb1bed-6b09-d1b8-be72-da28dc0cf647.png)
![スクリーンショット 2024-11-25 222204.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/bb0b18f5-9b2e-9d0a-24c5-1763058159b1.png)

実行すると復号化されたデータが表示されます。
実行後は`storage code: deciphertext`に数値がリスト型で格納されます。

![スクリーンショット 2024-11-25 222631.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/6062215a-d003-a75c-4c2c-8f45ed68f94d.png)
![スクリーンショット 2024-11-25 222633.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/a2ef9c75-9636-2947-a8c9-ecdfd811b2d5.png)

## おわりに

これにて数値を暗号化して復号化するまでの流れが完成しました。
データ引継ぎとして利用する際にはscoreboardからstorageにリスト型で格納したり、復号化されたデータから数値を取り出し、進行度を再設定したりの操作が必要になると思います。
この記事で提案した暗号化では数値を32進数に変換していますが、使用文字を増やしたり、扱う数値を正の値に限定して負の値に使用していた文字を数値変換に当てたりすることで暗号文の短縮になると思いますので、環境に合わせて仕様を変更してみてください。

この記事で解説した暗号化の試作品を利用したデータ引継ぎを実装したマップも配布していますので、興味のある方は遊んでやってください。

https://privatter.net/p/10889787

## おまけ
### キーボードの実装
ここまでは復号化する際にstorageを直接操作していましたが、プレイヤーが直感的に操作できるキーボードを実装し、コマンド実行を必要としない復号化を実現します。

配布データパックには、interaction、item_display、block_displayを利用したキーボードが実装されており、座標と水平方向を指定して召喚することができます。
水平方向は90の倍数を指定しなければ正常に召喚できないことに注意してください。

```mcfunction:Pos: [1.0, 2.0, 3.0], Rotation: [90.0, 0.0]を指定したい場合
function #keyboard:summon {x:"1.0",y:"2.0",z:"3.0",r:"90.0"}
```

![スクリーンショット 2024-11-26 003930.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3914993/5e7c06e0-7122-461e-82da-4bf9ee589b75.png)

召喚したキーボードで文字を入力し、En(Enter)をクリックすることで復号化を実行することができます。

### 暗号文の信頼度
「暗号化と復号化ができることは分かったけどでたらめな文字を入力して復号化できちゃうようなら暗号化した意味ないじゃんね。でたらめな文字から復号化できる確率ってどんなもんなの？」

チェックサムを用いるなど、正常な暗号文であるかを確認する操作を行っていますが、実際にどの程度効果があるか言及していませんでした。

暗号文に使われる文字と1文字目と2文字目に使われる文字の範囲が解析済みであると仮定した場合の復号成功率を試行回数を重ねることで無理やり出力させるプログラムを用意しました。

<details><summary>decryption_random.py (折りたたみ)</summary>

```python:decryption_random.py
import random
import time
import copy

random_char = ['2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','m','n','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','J','K','L','M','N','P','Q','R','S','T','U','V','W','X','Y','Z']

to_char = ['2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','m','n','p','q','r','s','t','u','v','w','x','y','z']
to_positive_char = ['A','B','C','D','E','F','G','H','J','K','L','M']
to_negative_char = ['N','P','Q','R','S','T','U','V','W','X','Y','Z']

def to_random(argument_char):
    return_int = {
        'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'J': 8, 'K': 9, 'L': 10, 'M': 11
    }
    return return_int.get(argument_char, -1)

def to_number(argument_char):
    return_int = {
        '2':  0, '3':  1, '4':  2, '5':  3, '6':  4, '7':  5, '8':  6, '9':  7, 'a':  8, 'b':  9, 'c': 10, 'd': 11, 'e': 12, 'f': 13, 'g': 14, 'h': 15, 'i': 16, 'j': 17, 'k': 18, 'm': 19, 'n': 20, 'p': 21, 'q': 22, 'r': 23, 's': 24, 't': 25, 'u': 26, 'v': 27, 'w': 28, 'x': 29, 'y': 30, 'z': 31, 'A': 32, 'B': 33, 'C': 34, 'D': 35, 'E': 36, 'F': 37, 'G': 38, 'H': 39, 'J': 40, 'K': 41, 'L': 42, 'M': 43, 'N': 44, 'P': 45, 'Q': 46, 'R': 47, 'S': 48, 'T': 49, 'U': 50, 'V': 51, 'W': 52, 'X': 53, 'Y': 54, 'Z': 55
    }
    return return_int.get(argument_char, -1)

def overflow(argument_int):
    argument_int = (argument_int + 4294967296) % 4294967296
    if 2147483647 < argument_int:
        argument_int -= 4294967296
    return argument_int

def yes_no():
    while True:
        yes_no_input = input('\n続行(y/n)：').lower()
        if yes_no_input in ['y', 'ye', 'yes']:
            return True
        elif yes_no_input in ['n', 'no']:
            return False

while True:
    charmin = int(input('生成文字列最小値：'))
    if charmin < 3:
        print('3 ..')
        break
    charmax = int(input('生成文字列最大値：'))
    if charmax < charmin:
        print(charmin,'..')
        break
    count_setting = int(input('試行回数(0=無限)：'))
    if count_setting < 0:
        print('0 ..')
        break
    count_run = 0
    count_success = 0
    stoptime = int(input('停止時間(1000=1sec)：'))
    if stoptime < 0:
        print('0 ..')
        break
    flag_success_stop = int(input('復号成功時に一時停止するか(0=一時停止しない,1=一時停止する)：'))
    if flag_success_stop != 0 and flag_success_stop != 1:
        print('0 or 1')
        break
    while True:
        print('====================')
        if 0 < count_setting:
            count_run += 1
            print('試行回数 =',count_run)
        ciphertext = []
        ciphertext_count = random.randint(charmin,charmax)
        ciphertext.append(random_char[random.randint(33,43)])
        ciphertext.append(random_char[random.randint(0,31)])
        for i in range(2,ciphertext_count):
            ciphertext.append(random_char[random.randint(0,55)])
        ciphertext_cmp = copy.deepcopy(ciphertext)
        print('ciphertext =',''.join(ciphertext))

        dechiphertext = []
        dechipher_score = 0
        dechipher_scale = 1
        checksum = 0
        flag_error = False
        flag_zero = False
        random_num = to_random(ciphertext[0])
        ciphertext.pop(0)
        if random_num == -1:
            flag_error = True
            print('不使用文字')
        if not ciphertext:
            flag_error = True
            print('2文字目不足')
        random_sum = random_num
        while True:
            if flag_error == True:
                break
            dechipher_num = to_number(ciphertext[0])
            ciphertext.pop(0)
            if dechipher_num == -1:
                flag_error = True
                print('不使用文字')
            if 0 <= dechipher_num and dechipher_num <= 31:
                flag_zero = False
                if dechipher_num == 0 and 1 < dechipher_scale:
                    flag_zero = True
                if dechipher_scale == 1073741824 and 3 <= dechipher_num:
                    flag_error = True
                    print('(32^6)*3以上の数値')
                if dechipher_scale < 1:
                    flag_error = True
                    print('7連続数値文字')
                if not ciphertext:
                    flag_error = True
                    print('終端文字不足')
                dechipher_num *= dechipher_scale
                dechipher_score = overflow(dechipher_score + dechipher_num)
                if -2147483648 < dechipher_score and dechipher_score < 0:
                    flag_error = True
                    print('((32^6)*3)+1以上の数値')
                dechipher_scale = overflow(dechipher_scale * 32)
            elif 32 <= dechipher_num and dechipher_num <= 43:
                if flag_zero == True:
                    flag_error = True
                    print('数値0終了')
                if dechipher_scale == 1:
                    flag_error = True
                    print('連続終端文字')
                dechipher_num -= 32
                checksum = overflow(checksum + dechipher_score)
                checksum %= 12
                if dechipher_num != checksum:
                    flag_error = True
                    print('チェックサム不一致')
                random_sum += random_num
                random_sum %= 12
                dechipher_score = overflow(dechipher_score - random_sum)
                dechiphertext.append(dechipher_score)
                dechipher_score = 0
                dechipher_scale = 1
            elif 44 <= dechipher_num and dechipher_num <= 55:
                if flag_zero == True:
                    flag_error = True
                    print('数値0終了')
                if dechipher_scale == 1:
                    flag_error = True
                    print('連続終端文字')
                if dechipher_score == 0:
                    flag_error = True
                    print('負終端数値0')
                dechipher_num -= 44
                dechipher_score *= -1
                checksum = overflow(checksum + dechipher_score)
                checksum %= 12
                if dechipher_num != checksum:
                    flag_error = True
                    print('チェックサム不一致')
                random_sum += random_num
                random_sum %= 12
                dechipher_score = overflow(dechipher_score - random_sum)
                dechiphertext.append(dechipher_score)
                dechipher_score = 0
                dechipher_scale = 1
            if not ciphertext:
                break
        if flag_error == True:
            print('復号不可')
            dechiphertext = []

        elif flag_error == False:
            count_success += 1
            print('\n復号成功\n\nrandom_num =',random_num,'\ndechiphertext =',dechiphertext)

            checksum = 0
            plaintext = dechiphertext
            random_sum = random_num
            reciphertext = [to_positive_char[random_num]]
            while True:
                random_sum += random_num
                random_sum %= 12
                plain_score = plaintext[0]
                plaintext.pop(0)
                plain_score = overflow(plain_score + random_sum)
                checksum = overflow(checksum + plain_score)
                checksum %= 12
                if 0 <= plain_score:
                    while True:
                        plain_num = plain_score % 32
                        plain_score //= 32
                        reciphertext.append(to_char[plain_num])
                        if plain_score <= 0:
                            break
                    reciphertext.append(to_positive_char[checksum])
                if plain_score <= -1:
                    plain_score *= -1
                    while True:
                        plain_num = plain_score % 32
                        plain_score //= 32
                        reciphertext.append(to_char[plain_num])
                        if plain_score <= 0:
                            break
                    reciphertext.append(to_negative_char[checksum])
                if not plaintext:
                    break
            print('reciphertext=',''.join(reciphertext))

            if ciphertext_cmp != reciphertext:
                print('\nミスマッチ')
                if yes_no() == False:
                    break
            else:
                if flag_success_stop == 1:
                    if yes_no() == False:
                        break

        if 0 < count_setting and count_run == count_setting:
            print('====================\n生成文字列範囲 =',charmin,'~',charmax,'\n復号成功回数 / 試行回数 =',count_success,'/',count_run,'\n復号成功率 =','{:.2f}'.format((count_success / count_run) * 100),'%')
            break

        time.sleep(stoptime/1000)

    break
```

</details>

暗号文の長さごとに100000回試行したときの復号成功率がこちら。

|暗号文の長さ|3|4|5|6|7|8|9|10|
|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|復号成功率(%)|3.58|1.93|1.21|0.76|0.43|0.26|0.05|0.03|

最も当たりやすい3文字でも3.58%のようです。引継ぎの際には数値の要素数が合わないといけないなど、更に条件が厳しくなるのである程度は信頼しても良いのではないでしょうか。

以上、スコアボードの値を暗号化してみた話でした。
ここまで読んでいただきありがとうございました。

## 訂正
1073741824以上の数値が復元できなかった問題を修正(2024/12/24)
8行目
誤：..3
正：3..
```mcfunction:data\code\function\decryption\score.mcfunction
- execute if score #dechipher_scale code_decryption matches 1073741824 if score #dechipher_num code_decryption matches ..3 run scoreboard players set #flag_error code_decryption 1
+ execute if score #dechipher_scale code_decryption matches 1073741824 if score #dechipher_num code_decryption matches 3.. run scoreboard players set #flag_error code_decryption 1
```