业务上的原因需要做个类似功能开关的设计,要求服务端能控制客户端的一些功能开启和关闭,但要求整个流程是安全的(如果报文被截胡并修改,客户端的某些危险功能就不能关闭了)。思来想去,最后决定使用
Curve25519对具体的功能内容进行加密,再使用配套Ed25519算法进行验签的形式进行
说明
- 这里我们假设一个常见的场景:
A对B发起询问,以获取某些需要进行加密的内容。我们对B向A发送的消息进行加密解密,以保证通信过程中不被第三者干扰。
前期准备
服务端
- 使用
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))
}
-
选一个地方生成即可,不过需要注意:
go中PrivateKey的生成结果是一个长度为64的列表,PublicKey的内容为其后32字节。而标准的Ed25519的密钥对,私钥和公钥的长度都为32,也就是说,privateKey的前32字节才是真实的私钥,但因为这个库中后续的各种操作都需要一个64字节的列表进行,所以这里不提取出真实的私钥。而base64编码的目的自然是为了方便储存和发送,我们需要生成一对密钥,用于客户端验证服务端发送的消息途中没有出现问题。这里展示我生成的密钥:-
私钥:
EsJERpGFpUvqbLMiqTUAxtZfuywCRyjQah2WJmdpl/dQqtZ01I7kju8D39zNwHzsDAcwLxAxZxTIF4a8Pj17BQ== -
公钥:
UKrWdNSO5I7vA9/czcB87AwHMC8QMWcUyBeGvD49ewU=
-
-
服务端需要保存:自己的私钥
-
客户端需要保存:服务端的公钥
Curve25519密钥交换
-
Curve25519需要在两端都生成一个密钥对。下面我简述一下整个消息的发送流程:-
客户端:我们假设消息刚开始由客户端发出,客户端需要在本地随机生成一个
Curve25519或X25519密钥对,将生成的公钥携带在消息中进行发出 -
服务端:获取到客户端生成的公钥,在本地随机生成一个
Curve25519或X25519密钥对,将自己的私钥与客户端的公钥进行计算,获得一个共享密钥(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,以保证最后的base64是url safe的(不会出现会导致错误解析的&等字符)