🔑

E2EE暗号化の仕組み

2020/12/17に公開

はじめに

E2EE暗号化が普及した背景には、疑似乱数生成アルゴリズムDual_EC_DRBGにバックドアが存在する可能性がある疑惑や、エドワード・スノーデン氏が内部告発したPRISMによる監視・盗聴行為などが明らかになり、高度な暗号化や安全性の高い暗号化技術が求められるようになりました。

E2EEとは

E2EE(End-to-end encryption)とは、利用者のみが鍵を持ち、端末間で暗号化されているためMITM攻撃などによる盗聴の対策になります。
前方秘匿性(FS)や楕円曲線上の離散対数問題の困難さから、安全性の高いことが特徴です。
第三者による解読を困難にするため事実上、利用者のみデータを復号出来ます。
一方で、犯罪捜査や違法コンテンツなどの対策が困難になることが問題視されてる。(サービス提供者も復号出来ないため)

E2EEが利用されてるサービス

  • ProtonMail
  • Wire
  • WhatsApp
  • LINE
  • Telegarm
  • Zoom
  • Signal
    など

仕組み

1. 鍵ペア生成

クライアント側でprivateKeypublicKeyを生成します。
本記事ではcurve25519^1を用いて鍵ペアを生成しています。

コード
type keyPair struct {
	priKey *[32]byte
	pubKey *[32]byte
}

func generateKey() *keyPair {
	privateKey, publicKey := new([32]byte), new([32]byte)
	if _, err := rand.Read(privateKey[:]); err != nil {
		log.Fatal(err)
	}
	curve25519.ScalarBaseMult(publicKey, privateKey)
	return &keyPair{
		priKey: privateKey,
		pubKey: publicKey,
	}
}

2. shared secret(共有秘密鍵)生成

1で生成した鍵ペアのprivateKeyと相手のpublicKeyを使い生成します。
shared secretが両者で異なれば暗号化された内容は復号出来ない。

コード
type keyPair struct {
	priKey *[32]byte
	pubKey *[32]byte
}

func (k *keyPair) KeyExcahenge(publicKey []byte) ([]byte, error) {
	return curve25519.X25519(k.priKey[:], publicKey)
}

3. 暗号化/復号化

AES-256-GCMで生成したshared secretを用いて暗号化する。
AAD(additional authenticated data)などは各自で実装してください。

コード
main.go
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

func randomBytes(size int) []byte {
	dst := make([]byte, size)
	if _, err := rand.Read(dst); err != nil {
		panic(err.Error())
	}
	return dst
}

func encrptKey(key []byte) []byte {
	salt := randomBytes(16)
	sha := sha256.New()
	sha.Write(salt)
	sha.Write(key)
	return sha.Sum(nil)
}

func encrypt() {
    // この鍵は生成した共有秘密鍵を使う
    key, _ := hex.DecodeString("4b062aa58974ab41900050afd7ad35cbb33342b10d499737ef011c44c95df218")
    encKey := encrptKey(key)
    plaintext := []byte("hello, world!")
    
    block, err := aes.NewCipher(encKey)
    if err != nil {
        panic(err.Error())
    }
    
    nonce := randomBytes(12)
    
    aesgcm, err := cipher.NewGCM(block)
    if err != nil {
        panic(err.Error())
    }

    ciphertext := aesgcm.Seal(nonce, nonce, plaintext, nil)
    // nonce(ciphertext[:12]) + data(ciphertext[12:])
    fmt.Printf("EncryptedData: %x\n", ciphertext)
}

func main() {
    encrypt()
}

追記:
鍵ペアを生成した後、privateKeyをデバイスに安全に保管しpublicKeyを登録用のAPIや公開鍵をやり取りする機能に登録する。
鍵登録時に鍵に、一意な識別子をバインドすることでより改竄に対し強固になる。
本記事ではAES-256-GCMで暗号化しました、認証付き暗号のため通信回路上で改竄された場合に検知することが出来ます。

Go1.20で導入されたcrypto/ecdhでX25519が使えるようになったので、golang.org/x/crypto/curve25519を置き換えることができます。

↓コードはこちらから見ることが出来ます
【鍵ペア~共有秘密鍵生成】
https://play.golang.org/p/kf1UMXQB9-Q
【暗号化】
https://play.golang.org/p/gellU07tSxO

終わりに

本記事ではGolangでの実装になりますが、他の言語でも利用することが出来るためE2EE暗号化に興味のある方はぜひ試してみてください。
内容に誤りや不備を発見してくれた方は、指摘くださると大変助かります。

Discussion