Week39 - 各种安全性演算法的应用 - 窃听、电子欺骗实作 [高智能方程序系列]

本文章同时发布於:


大家好,继上次Week38 - 各种安全性演算法的应用 - 概念篇之後,这次要介绍实做,以下程序码都会使用 Golang 撰写。

实作

以下实作大量参考Golang RSA encrypt and decrypt exampleGolang: aes-256-cbc examples (with iv, blockSize)

并且全部的范例都在此,请先 clone 下来会较好理解。

窃听(eavesdrop)

还记得上篇文章 - 「窃听(eavesdrop)是什麽?」章节防范的方法吗?就是

将传送的资料加密,这样就算坏蛋偷走了也不知道资料内容

接下来就要利用加密讯息的方式来实作,常见的加密有 2 种:

公开金钥加密(Public-key cryptography),又称非对称加密:

  • 拥有私钥与公钥 2 把钥匙
  • 公钥可以给任何人,私钥不可以给自己以外的人
  • 加密使用公钥,解密使用私钥

对称密钥演算法(Symmetric-key algorithm):

  • 拥有 1 把钥匙
  • 这把钥匙不可以给不够信任的人
  • 加密跟解密都用此钥匙

小明早餐店阿姨如果不够信任彼此,会采用公开金钥加密,因为采用对称密钥演算法的话,早餐店阿姨把钥匙给小明小明把这把钥匙公开给别人就不好了,这样大家都可以解开早餐店阿姨的加密资料,

使用公开金钥加密加在原本的循序图串起来就会如下:

看看程序码,进入week39/eavesdrop/public的资料夹:

$ cd week39/eavesdrop/public

里头有以下档案:

.
├── key    : 私钥
├── key.pub: 公钥
└── main.go: 程序码

keykey.pub是透过ssh-keygen这个软件来产生的,使用以下指令可以产生一组RSA公私钥:

$ ssh-keygen -t rsa -f key -m pem

输入後会询问是否要设定passphrase,这是一个安全密码,如果设定了,以後使用此私钥还要输入此安全密码才可使用,以增加安全性,此范例没有设定。

code 主要可以看main的部分,注解有解释流程,搭配循序图会较好理解:

// 大量参考: https://gist.github.com/mfridman/c0c5ece512f63d429c4589196a1d4242
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha512"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"log"
)

// LoadFile load the file to bytes
func LoadFile(path string) []byte {
	content, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}

	return content
}

// BytesToPrivateKey bytes to private key
func BytesToPrivateKey(priv []byte) *rsa.PrivateKey {
	block, _ := pem.Decode(priv)
	enc := x509.IsEncryptedPEMBlock(block)
	b := block.Bytes
	var err error
	if enc {
		log.Println("is encrypted pem block")
		b, err = x509.DecryptPEMBlock(block, nil)
		if err != nil {
			log.Fatal(err)
		}
	}
	key, err := x509.ParsePKCS1PrivateKey(b)
	if err != nil {
		log.Fatal(err)
	}
	return key
}

// BytesToPublicKey bytes to public key
func BytesToPublicKey(pub []byte) *rsa.PublicKey {
	block, _ := pem.Decode(pub)
	enc := x509.IsEncryptedPEMBlock(block)
	b := block.Bytes
	var err error
	if enc {
		log.Println("is encrypted pem block")
		b, err = x509.DecryptPEMBlock(block, nil)
		if err != nil {
			log.Fatal(err)
		}
	}
	ifc, err := x509.ParsePKIXPublicKey(b)
	if err != nil {
		log.Fatal(err)
	}
	key, ok := ifc.(*rsa.PublicKey)
	if !ok {
		log.Fatal("not ok")
	}
	return key
}

// EncryptWithPublicKey encrypts data with public key
func EncryptWithPublicKey(msg []byte, pub *rsa.PublicKey) []byte {
	hash := sha512.New()
	ciphertext, err := rsa.EncryptOAEP(hash, rand.Reader, pub, msg, nil)
	if err != nil {
		log.Fatal(err)
	}
	return ciphertext
}

// DecryptWithPrivateKey decrypts data with private key
func DecryptWithPrivateKey(ciphertext []byte, priv *rsa.PrivateKey) []byte {
	hash := sha512.New()
	plaintext, err := rsa.DecryptOAEP(hash, rand.Reader, priv, ciphertext, nil)
	if err != nil {
		log.Fatal(err)
	}
	return plaintext
}

func main() {
	// 早餐店阿姨产生公私钥
	privateKey := BytesToPrivateKey(LoadFile("./key"))
	// 公钥可以透过私要来取得,所以这边就不在载入公钥档案了
	publicKey := &privateKey.PublicKey

	// 小明获得早餐店阿姨的公钥,利用此公钥加密
	encryptedMsg := EncryptWithPublicKey([]byte("小明的付款密码: 12345"), publicKey)

	// 小明将 encryptedMsg 传送给早餐店阿姨

	// 早餐店阿姨使用私钥解开此讯息
	msg := DecryptWithPrivateKey(encryptedMsg, privateKey)
	fmt.Println(string(msg))
}

小明早餐店阿姨如果够信任彼此,甚至他们可能是同一个系统,那就不必担心小明早餐店阿姨的钥匙做坏事了,故可采用对称密钥演算法

使用对称密钥演算法加在原本的循序图串起来就会如下:

看看程序码,进入week39/eavesdrop/symmetric的资料夹:

$ cd week39/eavesdrop/symmetric

里头有以下档案:

.
└── main.go: 程序码

code 主要可以看main的部分,注解有解释流程,搭配循序图会较好理解:

// 大量参考: https://gist.github.com/yingray/57fdc3264b1927ef0f984b533d63abab
package main

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

func Ecrypt(plaintext string, key []byte, iv []byte, blockSize int) string {
	pad := func(ciphertext []byte, blockSize int, after int) []byte {
		padding := (blockSize - len(ciphertext)%blockSize)
		padtext := bytes.Repeat([]byte{byte(padding)}, padding)
		return append(ciphertext, padtext...)
	}

	bPlaintext := pad([]byte(plaintext), blockSize, len(plaintext))
	block, _ := aes.NewCipher(key)
	ciphertext := make([]byte, len(bPlaintext))
	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(ciphertext, bPlaintext)
	return hex.EncodeToString(ciphertext)
}

func Decrypt(ciphertext string, key []byte, iv []byte) string {
	unpad := func(ciphertext []byte) []byte {
		length := len(ciphertext)
		unpadding := int(ciphertext[length-1])
		return ciphertext[:(length - unpadding)]
	}

	decodeData, _ := hex.DecodeString(ciphertext)
	block, _ := aes.NewCipher(key)
	blockMode := cipher.NewCBCDecrypter(block, iv)
	originData := make([]byte, len(decodeData))
	blockMode.CryptBlocks(originData, decodeData)
	return string(unpad(originData))
}

func main() {
	// 小明与早餐店阿姨共同的钥匙
	key := []byte("di93bi39a^*(2i$2ajg9^ha9fj@hswe(")
	// 这边比较特别一点,由於是使用CBC演算法,所以在加密与解密时会多一个随机数iv,
	// 这可以让「相同的明文加密後,会产生不同的加密讯息」,以避免坏人透过相同的加密讯息来推断资讯
	iv := make([]byte, aes.BlockSize)
	if _, err := rand.Read(iv); err != nil {
		log.Fatal(err)
	}

	// 小明透过钥匙加密讯息
	plaintext := "小明的付款密码: 12345"
	ecryptMsg := Ecrypt(plaintext, key, iv, aes.BlockSize)

	// 早餐店阿姨透过钥匙解密讯息
	decryptMsg := Decrypt(ecryptMsg, key, iv)
	fmt.Println("早餐店阿姨", decryptMsg)
}

电子欺骗(spoofing)

还记得上篇文章 - 「电子欺骗(spoofing)是什麽?」章节防范的方法吗?就是

传输人员在资料上产生一笔独一无二的代码供另一端验证

接下来就要利用产生独一无二的代码来实作,方法有 2 种:

数位签章(Digital Signature):

  • 拥有私钥与公钥 2 把钥匙
  • 公钥可以给任何人,私钥不可以给自己以外的人
  • 使用私钥产生代码,公钥验证代码

讯息识别码(Message authentication code):

  • 拥有 1 把钥匙
  • 这把钥匙不可以给不够信任的人
  • 使用此钥匙来产生代码

小明早餐店阿姨如果不够信任彼此,会采用数位签章,因为采用讯息识别码的话,小明把钥匙给早餐店阿姨早餐店阿姨拿去冒名成小明就不好了,

使用数位签章加在原本的循序图串起来就会如下:

看看程序码,进入week39/spoofing/signature的资料夹:

$ cd week39/spoofing/signature

里头有以下档案:

.
├── badGuyKey     : 坏人的私钥
├── badGuyKey.pub : 坏人的公钥
├── goodGuykey    : 小明的私钥
├── goodGuykey.pub: 小明的公钥
└── main.go       : 程序码

钥匙都是透过ssh-keygen产生,可以参考上方窃听(eavesdrop)章节的讲解,就不赘述。

code 主要可以看main的部分,注解有解释流程,搭配循序图会较好理解:

// 大量参考: https://gist.github.com/mfridman/c0c5ece512f63d429c4589196a1d4242
package main

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha512"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"io/ioutil"
	"log"
)

// LoadFile load the file to bytes
func LoadFile(path string) []byte {
	content, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}

	return content
}

// BytesToPrivateKey bytes to private key
func BytesToPrivateKey(priv []byte) *rsa.PrivateKey {
	block, _ := pem.Decode(priv)
	enc := x509.IsEncryptedPEMBlock(block)
	b := block.Bytes
	var err error
	if enc {
		log.Println("is encrypted pem block")
		b, err = x509.DecryptPEMBlock(block, nil)
		if err != nil {
			log.Fatal(err)
		}
	}
	key, err := x509.ParsePKCS1PrivateKey(b)
	if err != nil {
		log.Fatal(err)
	}
	return key
}

func main() {
	// 坏人的私钥
	badGuyPrivateKey := BytesToPrivateKey(LoadFile("./badGuyKey"))
	// 小明的公钥,公钥可以透过私要来取得,所以这边就不在载入公钥档案了
	goodGuyPublicKey := BytesToPrivateKey(LoadFile("./goodGuyKey")).PublicKey

	// 坏人开始伪造小明的讯息
	messageBytes := []byte("小明餐点: 大冰红")
	hash := sha512.New()
	hash.Write(messageBytes)
	hashed := hash.Sum(nil)

	// 坏人用自己的私钥签名,并非小明的
	signature, err := rsa.SignPKCS1v15(rand.Reader, badGuyPrivateKey, crypto.SHA512, hashed)
	if err != nil {
		panic(err)
	}

	// 早餐店阿姨取得小明的公钥,利用此公钥验证之後发现不是小明传的讯息
	err = rsa.VerifyPKCS1v15(&goodGuyPublicKey, crypto.SHA512, hashed, signature)
	if err != nil {
		fmt.Println("Two signatures are not the same. Error: ", err)
		return
	}
}

小明早餐店阿姨如果够信任彼此,甚至他们可能是同一个系统,那就不必担心早餐店阿姨小明的钥匙做坏事了,故可采用讯息识别码

使用讯息识别码加在原本的循序图串起来就会如下:

看看程序码,进入week39/spoofing/HMAC的资料夹:

$ cd week39/spoofing/HMAC

里头有以下档案:

.
└── main.go : 程序码

code 主要可以看main的部分,注解有解释流程,搭配循序图会较好理解:

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

func hmacSha256(data string, secret string) string {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(data))
	return hex.EncodeToString(h.Sum(nil))
}

func main() {
	sharedSecret := "小明与早餐店阿姨的共同钥匙"
	badGuySecret := "坏人的钥匙"
	meals := "小明餐点: 大冰红"

	// 坏人利用自己的钥匙产生HMAC
	badGuyHMAC := hmacSha256(meals, badGuySecret)

	// 早餐店阿姨利用与小明共同的钥匙产生HMAC
	trueHMAC := hmacSha256(meals, sharedSecret)

	// 早餐店阿姨比对此两个HMAC,发现不同,故此讯息不是小明传送的
	if badGuyHMAC != trueHMAC {
		fmt.Println("Two HMACs are not the same.")
		return
	}
}

参考


<<:  【如何高效开发 ? 】测试驱动开发 | 3 大法则 + 5 大好处

>>:  Day 14 - 神经网络(D)NN 到 卷积神经网络CNN (1)

Day30: auto_ptr

常用的程序设计习惯是动态配置记忆体就是将某记忆体位置丢给指标後当在不需使用时才用delete手动回收...

建JS环境 Node Nodemon

我们飞快的结束惹html ,css,欢迎进入到下一个阶段JavaScrip。JS的助教很严,学习之前...

grep - 2 用更多Option

上篇的例子都是找档案 这次玩一下command | grep [option] pattern dp...

Chat & SignalR (Server)

今天想要练习一个简单的聊天室,输入使用者名称後,就可以进房聊天,主要使用 ASP.NET Singa...

谈谈 Spring boot Controller API 怎麽设计

说到了 controller 就不得不说一下 API,简单来说就是负责建立客户所需的内容和产生所需回...