ゼロ幅を利用した隠し文字埋め込み

# はじめに
ゼロ幅なる謎の文字を利用すると難読化や文字の埋め込みができます。
この記事では文字の埋め込みの仕組みとその実装をまとめています。

# そもそもゼロ幅って？
> ゼロ幅（ゼロはば、英語: zero-width）とは、コンピュータのタイプセッティングにおける概念で、一部の非表示文字のことを指す。 非表示文字は幅がない（表示されない）のが普通であるが、通常幅があるものが幅がないという意味で「ゼロ幅」と称している。(Wikipediaより)

- ゼロ幅接合子
- ゼロ幅非接合子
- ゼロ幅スペース
- ゼロ幅ノーブレークスペース

この四つがそれに該当し、今回の実装では上の三つを利用しました。

# 文字埋め込みの利用場面
1. 社外秘の書類が漏洩したときの犯人の特定
2. 自分の書いた記事だと証明するための署名

# 仕組み
ゼロ幅の文字はWeb上で表示されません。その性質を利用して埋め込みたい文字を2進数に変換し、0と1をゼロ幅の文字に置き換えます。
例えば、"a" の文字コード(UTF-8)は0x61(0b01100001), "あ" なら 0xE3 0x81 0x82(0b11100011 0b10000001 0b10000010)です。

# 実装
今回は0をゼロ幅非接合子(U+200C)、1をゼロ幅接合子(U+200D)にしました。また、"(())"で囲まれた文字が隠し文字として埋め込まれます。

## encode
```js
function encode(input) {
  // (())の中身を取り出し
  const targets = input.match(/\(\((.*?)\)\)/g);

  const translated_words = [];
  for (const target of targets) {
    // (())を除去  
    // いい感じの正規表現ができなかったため妥協
    const word = target.slice(2).slice(0, -2);
    if (word !== "")
      // 変換している場所を特定するためゼロ幅スペースでサンド
      translated_words.push("\u200B" + wordToZeroWidthCode(word) + "\u200B");
    else 
      translated_words.push("");  
  }

  let output = input;
  // 順番にゼロ幅のエンコード文字と置換
  for (const word of translated_words) {
    output = output.replace(/\(\((.*?)\)\)/, word);
  }

  return output;
}

function wordToZeroWidthCode(word) {
  let result = "";
  const encoder = new TextEncoder();
  // Uint8Arrayに変換 a -> [97], あ -> [ 227, 129, 130 ]
  const encoded_word = encoder.encode(word);
  for (let i = 0;i < encoded_word.length;i++) {
    // 2進数に変換
    const binary = encoded_word[i].toString(2);
    result += ("00000000" + binary).slice(-8) // 8桁にフォーマット
      .replace(/0/g, "\u200C")  // ゼロ幅非接合子
      .replace(/1/g, "\u200D"); // ゼロ幅接合子
  }
  return result;
}

const input = "社外秘　((共有先: 山田さん))新規プロジェクト○○について";
console.log(encode(input)); // 社外秘　​‍‍‍‌‌‍‌‍‍‌‌‌‌‍‌‍‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‌‍‌‌‍‍‍‌‌‍‌‌‌‍‌‌‍‍‍‍‌‌‍‌‍‍‌‌‌‌‍‌‍‍‌‌‌‍‌‌‌‌‌‍‍‍‌‍‌‌‌‍‌‌‌‌‌‍‍‍‌‌‍‌‍‍‌‍‍‌‌‌‍‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‍‍‌‌‍‌‍‌‌‍‌‍‍‌‌‌‌‍‍‍‌‌‌‍‍‍‌‌‌‌‌‌‍‍‌‌‍‌‍‌‍‍‍‍‌‌‌‍‍‍‌‌‌‌‌‍‌‍‌‌‍‌‌‍‍​新規プロジェクト○○について
```

Chromeのディベロッパーツールなどに張り付ければ社外秘と新規の間に何かたくさんあることが分かります。
TextEncoderがいい感じにしてくれるのでゼロ幅スペースでサンドする必要ないかもしれない


## decode
```js
function decode(input) {
  const targets = input.match(/\u200B(.*?)\u200B/g);

  const reqlaced_words = [];
  for (const target of targets) {
    const zero_width_code = target.slice(1).slice(0, -1); // \u200Bを除去
    if (zero_width_code !== "")
      reqlaced_words.push(zeroWidthCodeToWord(zero_width_code));
    else 
      reqlaced_words.push("");
  }

  let output = input;
  for (const word of reqlaced_words) {
    output = output.replace(/\u200B(.*?)\u200B/, word);
  }
  return output;
}

function zeroWidthCodeToWord(zero_width_code) {
  const binary_text = [];
  const binary_word = zero_width_code.replace(/\u200C/g, "0").replace(/\u200D/g, "1");
  
  for (let i = 0;i < binary_word.length;i += 8) {
    const byte = binary_word.substr(i, 8);
    // 2進数を10進数に変換
    binary_text.push(parseInt(byte, 2));
  }
    
  const decoder = new TextDecoder();
  return decoder.decode(new Uint8Array(binary_text));
}

const input = "社外秘　​‍‍‍‌‌‍‌‍‍‌‌‌‌‍‌‍‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‌‍‌‌‍‍‍‌‌‍‌‌‌‍‌‌‍‍‍‍‌‌‍‌‍‍‌‌‌‌‍‌‍‍‌‌‌‍‌‌‌‌‌‍‍‍‌‍‌‌‌‍‌‌‌‌‌‍‍‍‌‌‍‌‍‍‌‍‍‌‌‌‍‍‌‍‍‌‌‌‍‍‍‍‌‌‍‍‍‍‌‌‍‌‍‌‌‍‌‍‍‌‌‌‌‍‍‍‌‌‌‍‍‍‌‌‌‌‌‌‍‍‌‌‍‌‍‌‍‍‍‍‌‌‌‍‍‍‌‌‌‌‌‍‌‍‌‌‍‌‌‍‍​新規プロジェクト○○について";
console.log(decode(input)); // 社外秘　共有先: 山田さん新規プロジェクト○○について

```

# おわりに
ゼロ幅を利用すれば隠し文字を埋め込むことができるほかに難読化も可能です。
良ければこちらもどうぞ
[ゼロ幅を利用した難読化](https://qiita.com/ikesou/items/033c4afbdec3c4ea0323)
# 参考
ゼロ幅文字について
https://glodia.jp/blog/8142/

隠し情報をゼロ幅文字エンコードする仕組み
https://qiita.com/akicho8/items/db922164eb708da90e73