在GitHub和Gitlab上注册认证的公共钥匙可以由任何人获得。我制作了一个命令,可以使用该公共密钥并拥有私钥加密/解密文件。
此命令无需密码就可以处理加密文件。当然,如果用私钥注册了密码,则有必要。但是,无需将密码从发送者传递给收件人。
我命名为命令git-caesar
。不用担心,我不使用Caesar cipher。
这是一个新命令,所以可能有错误。
我不熟悉加密技术,所以我期待着了解更多了解的人。
此命令受QiiCipher的严重影响。
Qiicipher也是一种基于外壳脚本的工具,该工具使用github的RSA公共密钥对小文件进行加密。
Qiicipher旨在实现标准Shell功能 + OpenSSL/OpenSSH的最低要求,而此命令则以GO语言实现,以使其更易于处理。
GitHub和GitLab身份验证的公钥
我认为,在git push
期间,在github或gitlab中注册SSH的公共密钥很常见。
任何人都可以获取此注册的公钥。
- github公共密钥URL:
https://github.com/USER_NAME.keys
- gitlab公共密钥URL:
https://gitlab.com/USER_NAME.keys
这些公共密钥主要用于签名,但是某些算法也可以用于加密。
使用此公共密钥,实现无密码的加密/解密。
公共密钥加密算法支持RSA(关键长度1024位或更多位),ECDA和ED25519。
- RSA(关键长度为1024位或更多)
- 公钥前缀:
ssh-rsa
- 公钥前缀:
- ECDSA
- p256-公共密钥前缀:
ecdsa-sha2-nistp256
- p384-公共密钥前缀:
ecdsa-sha2-nistp384
- p521-公共密钥前缀:
ecdsa-sha2-nistp521
- p256-公共密钥前缀:
- ED25519-公共密钥前缀:
ssh-ed25519
其他,即秘密键的DSA,ECDSA-SK和ED25519-SK,而RSA不支持少于1024位的键长度。
- DSA-由于贬值和如何实施它的研究,因此被排除在Github/Gitlab之外。
- 公钥前缀:
ssh-dss
- 公钥前缀:
- ECDSA-SK,ED25519-SK-据我所研究,我已经确定这是不可能的。至少,它不能仅通过使用库来实现。
- 公钥前缀:
sk-ecdsa-sha2-nistp256@openssh.com
- 公钥前缀:
sk-ssh-ed25519@openssh.com
- 公钥前缀:
- RSA(密钥长度小于1024位) - 确定为安全问题。另外,由于关键长度很短,因此对密钥加密有限制。
消息主体加密
即使使用了公钥加密,也可以使用对称键密码学加密消息的正文。
此命令在AES-256-CBC模式下使用对称密钥加密方法AES。
package aes
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
)
func Encrypt(key, plaintext []byte) ([]byte, error) {
// pad the message with PKCS#7
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
padtext := append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...)
ciphertext := make([]byte, aes.BlockSize+len(padtext))
iv := ciphertext[:aes.BlockSize]
encMsg := ciphertext[aes.BlockSize:]
// generate initialization vector (IV)
_, err := rand.Read(iv)
if err != nil {
return nil, err
}
// encrypt message (AES-CBC)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(encMsg, padtext)
return ciphertext, nil
}
func Decrypt(key, ciphertext []byte) ([]byte, error) {
// extract the initial vector (IV)
iv := ciphertext[:aes.BlockSize]
encMsg := ciphertext[aes.BlockSize:]
// create an decrypter in CBC mode
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cbc := cipher.NewCBCDecrypter(block, iv)
// decrypt ciphertext
msgLen := len(encMsg)
decMsg := make([]byte, msgLen)
cbc.CryptBlocks(decMsg, encMsg)
// Unpad the message with PKCS#7
plaintext := decMsg[:msgLen-int(decMsg[msgLen-1])]
return plaintext, nil
}
首先,通过某种方法(例如随机数或密钥交换)准备共享密钥(32个字节= 256位)。
使用该共享密钥加密AES-256-CBC。
加密需要一个称为初始化向量(IV)的随机数。
静脉注射密码时就像盐一样,因此,即使对相同的数据进行了加密,密文也会有所不同。这很好。
此IV必须传递给收件人,因此我在Ciphertext的开头插入它。
接收时,将第一个块提取为IV,然后解码。
另外,AES-256-CBC需要与块长度相匹配的填充。
有几种填充方法,但是这次我们使用PKCS#7方法。
可以按原样传递密文,但重要的一点是如何与另一方共享用于加密的共享密钥。
对于RSA公共钥匙
RSA公共密钥仅限于足够小的数据,但也可以用于加密。
但是,原始的加密方法对于非熟练的人来说很难掌握(例如,在欧洲央行密码模式下使用块密码之类的错误很容易。因此,我使用了使用RSA的密码套件RSA-OAEP。
package rsa
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
)
func Encrypt(pubKey *rsa.PublicKey, plaintext []byte) ([]byte, error) {
return rsa.EncryptOAEP(sha256.New(), rand.Reader, pubKey, plaintext, []byte{})
}
func Decrypt(prvKey *rsa.PrivateKey, ciphertext []byte) ([]byte, error) {
return rsa.DecryptOAEP(sha256.New(), rand.Reader, prvKey, ciphertext, []byte{})
}
func Sign(prvKey *rsa.PrivateKey, message []byte) ([]byte, error) {
hash := sha256.Sum256(message)
return rsa.SignPKCS1v15(rand.Reader, prvKey, crypto.SHA256, hash[:])
}
func Verify(pubKey *rsa.PublicKey, message, sig []byte) bool {
hash := sha256.Sum256(message)
err := rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hash[:], sig)
return err == nil
}
由于测试的结果,如果RSA密钥长度约为800位,则似乎可以加密32字节键。
由于这次支持的密钥长度为1024位或更多位,因此可以在没有问题的情况下对32字节键进行加密。
使用收件人的RSA公钥对AES-256-CBC的共享密钥进行加密并发送给另一方,然后他们可以用自己的RSA私有密钥解密。
对于ECDSA公共钥匙
ECDSA是用于签名的算法。因此,它不能直接用于加密/解密。
与ECDA相关的是ECDH密钥交换算法,ECDSA密钥几乎可以像ECDH一样使用。
而且,如果您使用密钥交易所,则发件人使用“发件人的私钥”和“收件人的公钥”,并且接收器使用“收件人的私钥”和“发件人的公共密钥”,您可以通过交换密钥来获得相同的密钥。 P>
使用此交换密钥与AES等对称密钥加密系统加密/解密时,无需传递密钥本身。
package ecdsa
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"encoding/asn1"
"math/big"
"github.com/yoshi389111/git-caesar/caesar/aes"
)
func Encrypt(peersPubKey *ecdsa.PublicKey, message []byte) ([]byte, *ecdsa.PublicKey, error) {
curve := peersPubKey.Curve
// generate temporary private key
tempPrvKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, err
}
// key exchange
exchangedKey, _ := curve.ScalarMult(peersPubKey.X, peersPubKey.Y, tempPrvKey.D.Bytes())
sharedKey := sha256.Sum256(exchangedKey.Bytes())
// encrypt AES-256-CBC
ciphertext, err := aes.Encrypt(sharedKey[:], message)
if err != nil {
return nil, nil, err
}
return ciphertext, &tempPrvKey.PublicKey, nil
}
func Decrypt(prvKey *ecdsa.PrivateKey, peersPubKey *ecdsa.PublicKey, ciphertext []byte) ([]byte, error) {
curve := prvKey.Curve
// key exchange
exchangedKey, _ := curve.ScalarMult(peersPubKey.X, peersPubKey.Y, prvKey.D.Bytes())
sharedKey := sha256.Sum256(exchangedKey.Bytes())
// decrypt AES-256-CBC
return aes.Decrypt(sharedKey[:], ciphertext)
}
type sigParam struct {
R, S *big.Int
}
func Sign(prvKey *ecdsa.PrivateKey, message []byte) ([]byte, error) {
hash := sha256.Sum256(message)
r, s, err := ecdsa.Sign(rand.Reader, prvKey, hash[:])
if err != nil {
return nil, err
}
sig, err := asn1.Marshal(sigParam{R: r, S: s})
if err != nil {
return nil, err
}
return sig, nil
}
func Verify(pubKey *ecdsa.PublicKey, message, sig []byte) bool {
hash := sha256.Sum256(message)
signature := &sigParam{}
_, err := asn1.Unmarshal(sig, signature)
if err != nil {
return false
}
return ecdsa.Verify(pubKey, hash[:], signature.R, signature.S)
}
注意:正如稍后所述,每次都会生成发件人的密钥对。
用于ED25519公共钥匙
ED25519也是用于签名的算法。因此,它也不能用于加密/解密。
还与ED25519相关的是X25519密钥交换算法。
基于ED25519密钥的计算产生X25519密钥。
但是,由于有损的转换,不可能从反向X25519键制作ED25519密钥。
package ed25519
import (
"crypto/ecdh"
"crypto/ed25519"
"crypto/sha512"
"math/big"
)
func toX2519PrivateKey(edPrvKey *ed25519.PrivateKey) (*ecdh.PrivateKey, error) {
key := sha512.Sum512(edPrvKey.Seed())
return ecdh.X25519().NewPrivateKey(key[:32])
}
// p = 2^255 - 19
var p, _ = new(big.Int).SetString("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", 16)
var one = big.NewInt(1)
func toX25519PublicKey(edPubKey *ed25519.PublicKey) (*ecdh.PublicKey, error) {
// convert to big-endian
bigEndianY := toReverse(*edPubKey)
// turn off the first bit
bigEndianY[0] &= 0b0111_1111
y := new(big.Int).SetBytes(bigEndianY)
numer := new(big.Int).Add(one, y) // (1 + y)
denomInv := y.ModInverse(y.Sub(one, y), p) // 1 / (1 - y)
// u = (1 + y) / (1 - y)
u := numer.Mod(numer.Mul(numer, denomInv), p)
// convert to little-endian
littleEndianU := toReverse(u.Bytes())
// create x25519 public key
return ecdh.X25519().NewPublicKey(littleEndianU)
}
func toReverse(input []byte) []byte {
length := len(input)
output := make([]byte, length)
for i, b := range input {
output[length-i-1] = b
}
return output
}
使用X25519的密钥交换如下。
package ed25519
import (
"crypto/ecdh"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"github.com/yoshi389111/git-caesar/caesar/aes"
)
func Encrypt(otherPubKey *ed25519.PublicKey, message []byte) ([]byte, *ed25519.PublicKey, error) {
// generate temporary key pair
tempEdPubKey, tempEdPrvKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return nil, nil, err
}
// convert ed25519 public key to x25519 public key
xOtherPubKey, err := toX25519PublicKey(otherPubKey)
if err != nil {
return nil, nil, err
}
// convert ed25519 prevate key to x25519 prevate key
xPrvKey, err := toX2519PrivateKey(&tempEdPrvKey)
if err != nil {
return nil, nil, err
}
// key exchange
sharedKey, err := exchangeKey(xPrvKey, xOtherPubKey)
if err != nil {
return nil, nil, err
}
// encrypt AES-256-CBC
ciphertext, err := aes.Encrypt(sharedKey, message)
if err != nil {
return nil, nil, err
}
return ciphertext, &tempEdPubKey, nil
}
func Decrypt(prvKey *ed25519.PrivateKey, otherPubKey *ed25519.PublicKey, ciphertext []byte) ([]byte, error) {
// convert ed25519 public key to x25519 public key
xOtherPubKey, err := toX25519PublicKey(otherPubKey)
if err != nil {
return nil, err
}
// convert ed25519 prevate key to x25519 prevate key
xPrvKey, err := toX2519PrivateKey(prvKey)
if err != nil {
return nil, err
}
// key exchange
sharedKey, err := exchangeKey(xPrvKey, xOtherPubKey)
if err != nil {
return nil, err
}
// decrypt AES-256-CBC
return aes.Decrypt(sharedKey, ciphertext)
}
func exchangeKey(xPrvKey *ecdh.PrivateKey, xPubKey *ecdh.PublicKey) ([]byte, error) {
exchangedKey, err := xPrvKey.ECDH(xPubKey)
if err != nil {
return nil, err
}
sharedKey := sha256.Sum256(exchangedKey)
return sharedKey[:], nil
}
func Sign(prvKey *ed25519.PrivateKey, message []byte) ([]byte, error) {
hash := sha256.Sum256(message)
sig := ed25519.Sign(*prvKey, hash[:])
return sig, nil
}
func Verify(pubKey *ed25519.PublicKey, message, sig []byte) bool {
hash := sha256.Sum256(message)
return ed25519.Verify(*pubKey, hash[:], sig)
}
使用此交换键,可以以与ECDA相同的方式实现加密/解密。
多个公共钥匙,不同类型的密钥
github和gitlab可以有多个公共密钥。
在多个PC/环境上工作时,使用单个私钥会增加由于可移植性/复制而泄漏的可能性。
为了避免这种情况,我们将在每个环境中生成一个密钥对,并在github等上注册其公共密钥等。
在此命令中,我想让Ciphertext的接收器能够在这种情况下用任何私钥解密它。
另外,另一方的密钥算法与您自己的密钥算法不同是正常的。
所以我决定这样对其进行加密。
首先,加密消息主体的共享密钥是由随机数生成的。
- 对于RSA:
- 使用收件人的RSA公共密钥加密共享密钥。
- 将加密的共享密钥传递给另一方
- 对于ECDSA,ED25519:
- 生成一次性密钥对。
- 使用一次性私钥和收件人的公共密钥执行密钥交换。
- 用交换键加密共享密钥。
- 将加密的共享密钥和一次性的公共密钥传递给另一方
包含加密的共享密钥和一次性公钥的容器称为信封。
收件人选择可以用自己的私钥解密的信封,恢复共享键,并解密密码。
签名验证
除了加密/解密外,该命令还使用发件人的私钥签名“加密消息正文”。
收件人可以使用其公共密钥(例如GitHub)来检查签名。
使用github等的签名验证是可选的。仅在没有签名验证的情况下也可以解密。
密文文件结构
此命令生成的ciphertext是包含以下两个文件的zip文件。
-
caesar.json
-包含以下项目的JSON文件- 签名数据
- 签名者的公钥
- 版本信息
- 信封清单
-
caesar.cipher
-加密消息主体
请注意以下内容:
- 此ZIP文件在加密之前不保留文件名信息。如有必要,发件人应分别告知收件人文件名。
- 此ZIP文件不保留有关发件人公共密钥在何处的信息(GitHub帐户名称,URL等)。
- 仅加密一个文件。如果您立即加密多个文件,请提前存档。
安装
需要1.20或更高。
执行以下命令以安装和升级。
go install github.com/yoshi389111/git-caesar@latest
要卸载,运行以下命令。
go clean -i github.com/yoshi389111/git-caesar
用法
Usage:
git-caesar [options]
Application Options:
-h, --help print help and exit.
-v, --version print version and exit.
-u, --public=<target> github account, url or file.
-k, --private=<id_file> ssh private key file.
-i, --input=<input_file> the path of the file to read. default: stdin
-o, --output=<output_file> the path of the file to write. default: stdout
-d, --decrypt decryption mode.
-
-u
指定了对等公共密钥的位置。如果指定的看起来像GitHub用户名,则从https://github.com/USER_NAME.keys
中获取。如果它以http:
或https:
开头,则将从网络中获取。否则,将确定为文件路径。如果指定看起来像GitHub用户名的文件,请使用路径(例如-u ./octacat
)指定它。加密所需。为了解密,请执行签名验证,如果指定。 -
-k
指定您的私钥。如果未指定,它将按顺序搜索~/.ssh/id_ecdsa
,~/.ssh/id_ed25519
,~/.ssh/id_rsa
并使用第一个找到。 -
-i
输入文件。加密时要加密的明文文件。解密时,请指定要解密的密文文件。如果未指定选项,则从标准输入中读取。 -
-o
输出文件。如果未指定选项,则输出到标准输出。 - 为解密模式指定
-d
。如果未指定加密模式。
使用的示例
github用户octacat
加密文件secret.txt
并将其保存为sceret.zip
。
git-caesar -u octacat -i secret.txt -o secret.zip
在同一情况下,私钥使用~/.ssh/id_secret
。
git-caesar -u octacat -i secret.txt -o secret.zip -k ~/.ssh/id_secret
解密gitlab用户tanuki
的文件secret.zip
并将其保存为sceret.txt
。
git-caesar -d -u https://gitlab.com/tanuki.keys -i secret.zip -o secret.txt
相同的情况,没有签名验证。
git-caesar -d -i secret.zip -o secret.txt
GitHub存储库
github存储库在下面。