使用GITHUB的公共密钥无密码加密
#github #go #cryptography #gitlab

在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
  • 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一样使用。
而且,如果您使用密钥交易所,则发件人使用“发件人的私钥”和“收件人的公钥”,并且接收器使用“收件人的私钥”和“发件人的公共密钥”,您可以通过交换密钥来获得相同的密钥。

使用此交换密钥与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:
    1. 使用收件人的RSA公共密钥加密共享密钥。
    2. 将加密的共享密钥传递给另一方
  • 对于ECDSA,ED25519:
    1. 生成一次性密钥对。
    2. 使用一次性私钥和收件人的公共密钥执行密钥交换。
    3. 用交换键加密共享密钥。
    4. 将加密的共享密钥和一次性的公共密钥传递给另一方

包含加密的共享密钥和一次性公钥的容器称为信封。

收件人选择可以用自己的私钥解密的信封,恢复共享键,并解密密码。

签名验证

除了加密/解密外,该命令还使用发件人的私钥签名“加密消息正文”。
收件人可以使用其公共密钥(例如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存储库在下面。

https://github.com/yoshi389111/git-caesar