绝对安全?使用Curve25519和Ed25519为发送的消息加密

582 阅读7分钟

业务上的原因需要做个类似功能开关的设计,要求服务端能控制客户端的一些功能开启和关闭,但要求整个流程是安全的(如果报文被截胡并修改,客户端的某些危险功能就不能关闭了)。思来想去,最后决定使用Curve25519对具体的功能内容进行加密,再使用配套Ed25519算法进行验签的形式进行

说明

  • 这里我们假设一个常见的场景:AB发起询问,以获取某些需要进行加密的内容。我们对BA发送的消息进行加密解密,以保证通信过程中不被第三者干扰。

前期准备

服务端

  • 使用go语言搭建

客户端

  • 使用Flutter实现,请确保pubspec.yaml中拥有以下库:
name: untitled1
description: "A new Flutter project."

# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: '>=3.2.6 <4.0.0'
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  retrofit: ^4.1.0
  json_annotation: ^4.8.1
  flutter_lints: ^3.0.1
  cryptography: ^2.7.0
  cryptography_flutter: ^2.3.2
  dio: ^5.4.0
  logger: ^2.0.2+1

dev_dependencies:
  flutter_test:
    sdk: flutter

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

客户端

绘制页面

  • 我们只要展现一个简单的页面即可,一行是A发出去的消息,一行是从B接受到的消息

  • main.dart

import 'package:flutter/material.dart';
import 'package:untitled1/home_page.dart';

void main(){
  runApp(MyApp());
}
class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: HomePage(),
    );
  }
}
  • home_page.dart
import 'dart:math';

import 'package:flutter/material.dart';

class HomePage extends StatefulWidget{
  String content = "Hello Bob!";
  String received = "";
  @override
  State<StatefulWidget> createState() => HomePageState();
}
class HomePageState extends State<HomePage>{
  @override
  void initState() {
    super.initState();
  }
  @override
  void dispose() {
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          Text('Send from A: ${widget.content}'),
        Text('Received from B: ${widget.received}'),
        ],
      )
    );
  }
}

消息加密

生成Ed25519密钥对

  • 如果你想了解Ed25519,可以通过这里:Ed25519,也可以在其它博客找到更通俗易懂的解释

  • 我们需要生成一对密钥对,消息的发送者负责使用私钥对消息进行签名操作,而消息的接收者负责使用公钥对消息的内容进行验签,证明消息的来源没问题(不是别人伪冒发送者发送的错误消息)

  • 生成操作在两端都可以进行

  • 客户端:

Future<void> generateKey() async {
    var ed25519 = FlutterEd25519(Ed25519());
    var keyPair = await ed25519.newKeyPair();
    var privateKeyBytes = await keyPair.extractPrivateKeyBytes();
    var publicKey = await keyPair.extractPublicKey();
    var publicKeyBytes = publicKey.bytes;
    Logger().i("privateKey: ${base64Encode(privateKeyBytes)}");
    Logger().i("publicKey: ${base64Encode(publicKeyBytes)}");
}
  • 服务端:
func generateKey() {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	log.Println("privateKey:", base64.StdEncoding.EncodeToString(privateKey))
	log.Println("publicKey:", base64.StdEncoding.EncodeToString(publicKey))
}
  • 选一个地方生成即可,不过需要注意:goPrivateKey的生成结果是一个长度为64的列表,PublicKey的内容为其后32字节。而标准的Ed25519的密钥对,私钥和公钥的长度都为32,也就是说,privateKey的前32字节才是真实的私钥,但因为这个库中后续的各种操作都需要一个64字节的列表进行,所以这里不提取出真实的私钥。而base64编码的目的自然是为了方便储存和发送,我们需要生成一对密钥,用于客户端验证服务端发送的消息途中没有出现问题。这里展示我生成的密钥:

    • 私钥:EsJERpGFpUvqbLMiqTUAxtZfuywCRyjQah2WJmdpl/dQqtZ01I7kju8D39zNwHzsDAcwLxAxZxTIF4a8Pj17BQ==

    • 公钥:UKrWdNSO5I7vA9/czcB87AwHMC8QMWcUyBeGvD49ewU=

  • 服务端需要保存:自己的私钥

  • 客户端需要保存:服务端的公钥

Curve25519密钥交换

  • Curve25519需要在两端都生成一个密钥对。下面我简述一下整个消息的发送流程:

    • 客户端:我们假设消息刚开始由客户端发出,客户端需要在本地随机生成一个Curve25519X25519密钥对,将生成的公钥携带在消息中进行发出

    • 服务端:获取到客户端生成的公钥,在本地随机生成一个Curve25519X25519密钥对,将自己的私钥客户端的公钥进行计算,获得一个共享密钥(SharedKey),接下来使用任一对称加密算法明文进行加密,将生成的公钥和密文一同发送给客户端

    • 客户端:接收到服务端生成的公钥和密文,将服务端的公钥自己之前生成的私钥进行计算,获得一个共享密钥(ShardKey),通过相同的对称加密算法对密文解密,得到明文

    • 注意:Curve25519算法保证两边最后计算得到的共享密钥是完全相同的,如果最后计算得到的结果不同,可能是数据传输过程中被他人修改了,即使这样,他人也无法获取密文中的内容

  • 服务端:

package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/ed25519"
	"crypto/rand"
	"encoding/base64"
	"github.com/gin-gonic/gin"
	"golang.org/x/crypto/curve25519"
	"log"
)

type Message struct {
	Content   string `json:"content,omitempty"`
	PublicKey string `json:"public_key,omitempty"`
	Sign      string `json:"sign,omitempty"`
}
type InternalContent struct {
	Data []string `json:"data,omitempty"`
}

const signPrivateKey = "dwCJcOXj/46vBlSlcXHFTLvnII1gVCY8pFYqNfTXwIOq2NkLWFYg2GwZ9aU8Pq8rxahoa1ZRjhdGm6yPD7aQ4Q=="

func server(clientX25519PublicKey string) Message {
	/// clientPublicKey: 客户端发来的X25519公钥,用于计算shareKey
	/// privateKey: 本地生成的Ed25519私钥,用于最后对内容的签名
	var clientPublicKey, privateKey [32]byte
	/// 将本地的Ed25519私钥准备好,用于最后的签名
	signPriKey, _ := base64.StdEncoding.DecodeString(signPrivateKey)
	/// ----------- 随机生成X25519密钥 -----------
	_, err := rand.Read(privateKey[:])
	if err != nil {
		panic(err)
		return Message{}
	}
	privateKey[0] &= 248
	privateKey[31] &= 127
	privateKey[31] |= 64
	/// ----------- 随机生成X25519密钥 -----------

	/// 解码客户端发来的X25519公钥
	publicKeyDec, _ := base64.URLEncoding.DecodeString(clientX25519PublicKey)
	copy(clientPublicKey[:], publicKeyDec)

	/// 将客户端发来的X25519公钥和服务端生成的私钥计算shareKey
	sharedKey, err := curve25519.X25519(privateKey[:], clientPublicKey[:])
	if err != nil {
		panic(err)
	}

	/// 通过本地随机生成的X25519计算相配套的公钥
	publicKey, _ := curve25519.X25519(privateKey[:], curve25519.Basepoint)

	/// 将计算得到的shareKey和明文使用对称加密算法进行加密
	/// 这里用的是ACM-GCM 256bits
	enc, _ := encrypt(sharedKey, "Hello Alice")
	return Message{
		Content:   base64.StdEncoding.EncodeToString(enc),
		PublicKey: base64.StdEncoding.EncodeToString(publicKey[:]),
		/// 注意这里,把加密后的内容进行签名
		Sign: base64.StdEncoding.EncodeToString(ed25519.Sign(signPriKey, enc)),
	}

}
func main() {
	router := gin.Default()
	router.GET("/message", func(context *gin.Context) {
		key := context.Query("key")
		response := server(key)
		context.JSON(200, response)
	})
	router.Run(":9000")
}

// / 使用shareKey将明文加密
func encrypt(key []byte, plaintext string) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		panic(err)
		return nil, err
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		panic(err)
		return nil, err
	}

	nonce := make([]byte, gcm.NonceSize())
	if _, err := rand.Read(nonce); err != nil {
		panic(err)
		return nil, err
	}
	ciphertext := gcm.Seal(nonce, nonce, []byte(plaintext), nil)
	return ciphertext, nil
}
func generateKey() {
	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		panic(err)
	}
	log.Println("privateKey:", base64.StdEncoding.EncodeToString(privateKey))
	log.Println("publicKey:", base64.StdEncoding.EncodeToString(publicKey))
}
  • 客户端:
import 'dart:convert';
import 'dart:ffi';
import 'dart:math';

import 'package:cryptography/cryptography.dart';
import 'package:cryptography_flutter/cryptography_flutter.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';

class HomePage extends StatefulWidget{
  String content = "Hello Bob!";
  String received = "";
  @override
  State<StatefulWidget> createState() => HomePageState();
}
class HomePageState extends State<HomePage>{
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'Send from A: ${widget.content}',
            style: TextStyle(
              fontSize: 30,
            ),
          ),
          FutureBuilder(
            future: signAndDecryptMessage(),
            builder: (content,snapshot){
              if(!snapshot.hasData) return SizedBox.shrink();
              return Text(
                'Received from B: ${snapshot.data}',
                style: TextStyle(
                  fontSize: 30,
                ),
              );
            }
          ),
        ],
      )
    );
  }
  Future<String> signAndDecryptMessage() async {
    var signPublicKey = "UKrWdNSO5I7vA9/czcB87AwHMC8QMWcUyBeGvD49ewU=";
    var x25519 = FlutterX25519(X25519());
    var ed25519 = FlutterEd25519(Ed25519());
    var keyPair = await x25519.newKeyPair();
    var publicKey = await keyPair.extractPublicKey();
    var dio = Dio()..interceptors.add(
      LogInterceptor(
        requestBody:  true,
        responseBody: true,
      )
    );
    var response = await dio.get(
        "http://10.129.24.45:9000/message?key=${base64UrlEncode(
            publicKey.bytes)}");
    var json = response.data as Map<String, dynamic>;
    var sign = await ed25519.verify(
        base64Decode(json["content"]),
        signature: Signature(
          base64Decode(json["sign"]),
          publicKey: SimplePublicKey(
              base64Decode(signPublicKey),
              type: KeyPairType.ed25519
          ),
        )
    );
    if (!sign) Logger().e("Sign Failed");
    var shareKey = await x25519.sharedSecretKey(
        keyPair: keyPair,
        remotePublicKey: SimplePublicKey(
            base64Decode(json["public_key"]),
            type: KeyPairType.x25519
        )
    );
    var secretKey = SecretKey(await shareKey.extractBytes());
    var algorithm = AesGcm.with256bits();
    var plainText = await algorithm.decryptString(
        SecretBox.fromConcatenation(
          base64Decode(json["content"]),
          nonceLength: AesGcm.defaultNonceLength,
          macLength: AesGcm.aesGcmMac.macLength,
        ),
        secretKey: secretKey
    );
    Logger().i("plainText: ${plainText}");
    return plainText;
  }
}
  • 注意:这里在用url的参数追加key时,使用了baseUrlEncode,以保证最后的base64url safe的(不会出现会导致错误解析的&等字符)