mirror of
https://github.com/charlienet/go-mixed.git
synced 2025-07-18 00:22:41 +08:00
加密包装
This commit is contained in:
87
crypto/aes.go
Normal file
87
crypto/aes.go
Normal file
@ -0,0 +1,87 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
)
|
||||
|
||||
type aesBlock struct {
|
||||
blockCipher
|
||||
key []byte
|
||||
}
|
||||
|
||||
func Aes(key []byte) *aesBlock {
|
||||
return &aesBlock{key: key}
|
||||
}
|
||||
|
||||
type aesEcbBlock struct {
|
||||
*aesBlock
|
||||
}
|
||||
|
||||
func (o *aesBlock) newCipher() (cipher.Block, error) {
|
||||
return aes.NewCipher(o.key)
|
||||
}
|
||||
|
||||
func (o *aesBlock) ECB() *aesEcbBlock {
|
||||
return &aesEcbBlock{o}
|
||||
}
|
||||
|
||||
var (
|
||||
defaultAesIV = make([]byte, aes.BlockSize)
|
||||
)
|
||||
|
||||
type aesCbcBlock struct {
|
||||
*aesBlock
|
||||
iv []byte
|
||||
}
|
||||
|
||||
func (o *aesBlock) CBC() *aesCbcBlock {
|
||||
return &aesCbcBlock{
|
||||
aesBlock: o,
|
||||
iv: defaultAesIV,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *aesCbcBlock) WithIV(iv []byte) *aesCbcBlock {
|
||||
o.iv = iv
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *aesEcbBlock) Encrypt(msg []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.ecbEncrypt(block, msg)
|
||||
}
|
||||
|
||||
func (o *aesEcbBlock) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.ecbDecrypt(block, cipherText)
|
||||
}
|
||||
|
||||
func (o *aesCbcBlock) Encrypt(msg []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := cipher.NewCBCEncrypter(block, o.iv)
|
||||
return o.encrypt(c, msg)
|
||||
}
|
||||
|
||||
func (o *aesCbcBlock) Decrypt(chiperText []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := cipher.NewCBCEncrypter(block, o.iv)
|
||||
return o.decrypt(c, chiperText)
|
||||
}
|
58
crypto/block.go
Normal file
58
crypto/block.go
Normal file
@ -0,0 +1,58 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type blockCipher struct {
|
||||
}
|
||||
|
||||
func (b *blockCipher) ecbEncrypt(block cipher.Block, data []byte) ([]byte, error) {
|
||||
bs := block.BlockSize()
|
||||
data = PKCS7Padding(data, bs)
|
||||
|
||||
out := make([]byte, len(data))
|
||||
dst := out
|
||||
for len(data) > 0 {
|
||||
block.Encrypt(dst, data[:bs])
|
||||
data = data[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *blockCipher) ecbDecrypt(block cipher.Block, cipherText []byte) ([]byte, error) {
|
||||
bs := block.BlockSize()
|
||||
if len(cipherText)%bs != 0 {
|
||||
return nil, errors.New("DecryptDES crypto/cipher: input not full blocks")
|
||||
}
|
||||
|
||||
out := make([]byte, len(cipherText))
|
||||
dst := out
|
||||
for len(cipherText) > 0 {
|
||||
block.Decrypt(dst, cipherText[:bs])
|
||||
cipherText = cipherText[bs:]
|
||||
dst = dst[bs:]
|
||||
}
|
||||
|
||||
return PKCS7UnPadding(out), nil
|
||||
}
|
||||
|
||||
func (b *blockCipher) encrypt(block cipher.BlockMode, data []byte) ([]byte, error) {
|
||||
|
||||
data = PKCS7Padding(data, block.BlockSize())
|
||||
|
||||
out := make([]byte, len(data))
|
||||
block.CryptBlocks(out, data)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (b *blockCipher) decrypt(block cipher.BlockMode, cipherText []byte) ([]byte, error) {
|
||||
|
||||
out := make([]byte, len(cipherText))
|
||||
block.CryptBlocks(out, cipherText)
|
||||
|
||||
return PKCS7UnPadding(out), nil
|
||||
}
|
95
crypto/des.go
Normal file
95
crypto/des.go
Normal file
@ -0,0 +1,95 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/des"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type desInstance struct {
|
||||
blockCipher
|
||||
key []byte
|
||||
}
|
||||
|
||||
func Des(key []byte) *desInstance {
|
||||
return &desInstance{key: key}
|
||||
}
|
||||
|
||||
// 包含des和3des,根据不同的密钥长度计算
|
||||
func (o *desInstance) newCipher() (cipher.Block, error) {
|
||||
switch len(o.key) {
|
||||
case 8:
|
||||
return des.NewCipher(o.key)
|
||||
case 24:
|
||||
return des.NewTripleDESCipher(o.key)
|
||||
default:
|
||||
return nil, errors.New("crypto/des: invalid key size " + strconv.Itoa(len(o.key)))
|
||||
}
|
||||
}
|
||||
|
||||
type desEcb struct {
|
||||
*desInstance
|
||||
}
|
||||
|
||||
func (o *desInstance) ECB() *desEcb {
|
||||
return &desEcb{desInstance: o}
|
||||
}
|
||||
|
||||
func (o *desEcb) Encrypt(data []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.ecbEncrypt(block, data)
|
||||
}
|
||||
|
||||
func (o *desEcb) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.ecbDecrypt(block, cipherText)
|
||||
}
|
||||
|
||||
var (
|
||||
defaultIV = make([]byte, des.BlockSize)
|
||||
)
|
||||
|
||||
type desCbc struct {
|
||||
*desInstance
|
||||
iv []byte
|
||||
}
|
||||
|
||||
func (o *desInstance) Cbc() *desCbc {
|
||||
return &desCbc{desInstance: o, iv: defaultIV}
|
||||
}
|
||||
|
||||
func (o *desCbc) WithIV(iv []byte) *desCbc {
|
||||
o.iv = iv
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *desCbc) Encrypt(data []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e := cipher.NewCBCEncrypter(block, o.iv)
|
||||
return o.encrypt(e, data)
|
||||
}
|
||||
|
||||
func (o *desCbc) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
block, err := o.newCipher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d := cipher.NewCBCDecrypter(block, o.iv)
|
||||
|
||||
return o.decrypt(d, cipherText)
|
||||
}
|
38
crypto/des_test.go
Normal file
38
crypto/des_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/des"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/crypto"
|
||||
)
|
||||
|
||||
func TestDes(t *testing.T) {
|
||||
key, _ := hex.DecodeString("0123456789ABCDEF")
|
||||
msg, _ := hex.DecodeString("F0A2B07E64DD2C25")
|
||||
|
||||
c, err := crypto.Des(key).ECB().Encrypt(msg)
|
||||
t.Log(hex.EncodeToString(c), err)
|
||||
|
||||
c, err = crypto.Des(key).Cbc().Encrypt(msg)
|
||||
t.Log(hex.EncodeToString(c), err)
|
||||
|
||||
d, err := crypto.Des(key).Cbc().Decrypt(c)
|
||||
t.Log(hex.EncodeToString(d), err)
|
||||
}
|
||||
|
||||
func TestTripleDES(t *testing.T) {
|
||||
key, _ := hex.DecodeString("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF")
|
||||
block, err := des.NewTripleDESCipher(key)
|
||||
t.Log(block, err, block.BlockSize())
|
||||
}
|
||||
|
||||
func TestAes(t *testing.T) {
|
||||
t.Log(aes.BlockSize)
|
||||
key, _ := hex.DecodeString("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF")
|
||||
msg, _ := hex.DecodeString("F0A2B07E64DD2C25")
|
||||
c, err := crypto.Aes(key).ECB().Encrypt(msg)
|
||||
t.Log(hex.EncodeToString(c), err)
|
||||
}
|
100
crypto/ecdsa.go
Normal file
100
crypto/ecdsa.go
Normal file
@ -0,0 +1,100 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ecdsaOptions struct {
|
||||
hashOptions
|
||||
prv *ecdsa.PrivateKey
|
||||
pub *ecdsa.PublicKey
|
||||
}
|
||||
|
||||
type Option interface {
|
||||
apply(*ecdsaOptions) error
|
||||
}
|
||||
|
||||
type hash2Option interface {
|
||||
apply(*hashOptions) error
|
||||
}
|
||||
|
||||
type privateKeyOption []byte
|
||||
|
||||
func (p privateKeyOption) apply(opts *ecdsaOptions) error {
|
||||
block, _ := pem.Decode(p)
|
||||
prv, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.prv = prv
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type publicKeyOption []byte
|
||||
|
||||
func (p publicKeyOption) apply(opts *ecdsaOptions) error {
|
||||
block, _ := pem.Decode(p)
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.pub = pub.(*ecdsa.PublicKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
type hashOption crypto.Hash
|
||||
|
||||
func (p hashOption) apply(opts *hashOptions) error {
|
||||
opts.h = crypto.Hash(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewEcdsa(h Hash, opts ...Option) (*ecdsaOptions, error) {
|
||||
i := &ecdsaOptions{}
|
||||
|
||||
sh := crypto.Hash(h)
|
||||
if !sh.Available() {
|
||||
return nil, errors.New("unknown hash value " + strconv.Itoa(int(h)))
|
||||
}
|
||||
|
||||
i.h = sh
|
||||
|
||||
for _, v := range opts {
|
||||
if err := v.apply(i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func WithHash2(h Hash) hash2Option {
|
||||
return hashOption(h)
|
||||
}
|
||||
|
||||
func ParsePrivateKey(pem []byte) Option {
|
||||
return privateKeyOption(pem)
|
||||
}
|
||||
|
||||
func ParsePublicKey(pem []byte) Option {
|
||||
return publicKeyOption(pem)
|
||||
}
|
||||
|
||||
func (opt *ecdsaOptions) Verify(msg, rText, sText []byte) bool {
|
||||
var r, s big.Int
|
||||
_ = r.UnmarshalText(rText)
|
||||
_ = s.UnmarshalText(sText)
|
||||
|
||||
sum := opt.getHash(msg)
|
||||
|
||||
return ecdsa.Verify(opt.pub, sum, &r, &s)
|
||||
}
|
35
crypto/ecdsa_test.go
Normal file
35
crypto/ecdsa_test.go
Normal file
@ -0,0 +1,35 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/crypto"
|
||||
)
|
||||
|
||||
func TestEsda(t *testing.T) {
|
||||
|
||||
prv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
|
||||
t.Log(err)
|
||||
|
||||
ecd, err := x509.MarshalECPrivateKey(prv)
|
||||
t.Log(err)
|
||||
|
||||
secp256r1, _ := asn1.Marshal(asn1.ObjectIdentifier{1, 2, 840, 10045, 3, 1, 7})
|
||||
fmt.Println(string(pem.EncodeToMemory(&pem.Block{Type: "EC PARAMETERS", Bytes: secp256r1})))
|
||||
b := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: ecd})
|
||||
fmt.Println(string(b))
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
ecdsa, err := crypto.NewEcdsa(crypto.SHA1)
|
||||
t.Log(err)
|
||||
|
||||
_ = ecdsa
|
||||
}
|
15
crypto/padding.go
Normal file
15
crypto/padding.go
Normal file
@ -0,0 +1,15 @@
|
||||
package crypto
|
||||
|
||||
import "bytes"
|
||||
|
||||
func PKCS7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
func PKCS7UnPadding(src []byte) []byte {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
return src[:(length - unpadding)]
|
||||
}
|
116
crypto/rsa.go
Normal file
116
crypto/rsa.go
Normal file
@ -0,0 +1,116 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
||||
|
||||
type rsaInstance struct {
|
||||
hashOptions
|
||||
prk *rsa.PrivateKey
|
||||
puk *rsa.PublicKey
|
||||
}
|
||||
|
||||
type rsaOption func(o *rsaInstance) error
|
||||
|
||||
func NewRsa(h Hash, opts ...rsaOption) (*rsaInstance, error) {
|
||||
o := &rsaInstance{}
|
||||
|
||||
sh := crypto.Hash(h)
|
||||
if !sh.Available() {
|
||||
return nil, errors.New("unknown hash value " + strconv.Itoa(int(h)))
|
||||
}
|
||||
|
||||
o.h = sh
|
||||
|
||||
for _, f := range opts {
|
||||
if err := f(o); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 未设置私钥时随机生成密钥
|
||||
if o.prk == nil {
|
||||
prk, err := rsa.GenerateKey(rand.Reader, defaultRsaBits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.prk = prk
|
||||
}
|
||||
|
||||
// 公钥未设置时从私钥导出
|
||||
if o.puk == nil {
|
||||
o.puk = &o.prk.PublicKey
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func ParsePKCS1PrivateKey(p []byte) rsaOption {
|
||||
return func(o *rsaInstance) error {
|
||||
block, _ := pem.Decode(p)
|
||||
if block == nil {
|
||||
return errors.New("failed to decode private key")
|
||||
}
|
||||
|
||||
prk, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.prk = prk
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePKIXPublicKey(p []byte) rsaOption {
|
||||
return func(o *rsaInstance) error {
|
||||
block, _ := pem.Decode(p)
|
||||
if block == nil {
|
||||
return errors.New("failed to decode public key")
|
||||
}
|
||||
|
||||
k, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
puk := k.(*rsa.PublicKey)
|
||||
|
||||
o.puk = puk
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o *rsaInstance) Encrypt(msg []byte) ([]byte, error) {
|
||||
return rsa.EncryptPKCS1v15(rand.Reader, o.puk, msg)
|
||||
}
|
||||
|
||||
func (o *rsaInstance) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
return rsa.DecryptPKCS1v15(rand.Reader, o.prk, ciphertext)
|
||||
}
|
||||
|
||||
func (o *rsaInstance) Sign(msg []byte) ([]byte, error) {
|
||||
hashed := o.getHash(msg)
|
||||
sign, err := rsa.SignPKCS1v15(rand.Reader, o.prk, o.h, hashed)
|
||||
return sign, err
|
||||
}
|
||||
|
||||
func (o *rsaInstance) Verify(msg, sign []byte) bool {
|
||||
hashed := o.getHash(msg)
|
||||
if err := rsa.VerifyPKCS1v15(o.puk, o.h, hashed, sign); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
129
crypto/rsa_test.go
Normal file
129
crypto/rsa_test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/crypto"
|
||||
)
|
||||
|
||||
func TestRsaSign(t *testing.T) {
|
||||
rsa, err := crypto.NewRsa(crypto.SHA256)
|
||||
t.Log(rsa, err)
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := rsa.Sign(msg)
|
||||
t.Log(base64.StdEncoding.EncodeToString(sign))
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
t.Log(rsa.Verify(msg, sign))
|
||||
}
|
||||
|
||||
func TestEncrypt(t *testing.T) {
|
||||
rsa, err := crypto.NewRsa(crypto.SHA1)
|
||||
t.Log(rsa, err)
|
||||
|
||||
msg := []byte("123456")
|
||||
cipherText, err := rsa.Encrypt(msg)
|
||||
t.Log(base64.StdEncoding.EncodeToString(cipherText), err)
|
||||
|
||||
decrypted, err := rsa.Decrypt(cipherText)
|
||||
t.Log(string(decrypted), err)
|
||||
}
|
||||
|
||||
const (
|
||||
pkBytes = `-----BEGIN RSA PRIVATE KEY-----
|
||||
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
|
||||
-----END RSA PRIVATE KEY-----`
|
||||
|
||||
badPubBytes = `-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDWKn2UPL1SmlufOkgMJHDqLhjf
|
||||
vyT55z2MZRzeLqB3u8YlRLUD8zi3kmQy6loHQFu0FR7en/DI9EWRXARxMKhbH+CM
|
||||
yzwmdh9QlzpMrQk0p4e5VtM5pXX9d4B4XxKBXBrmp2n/1D3+oovzD6p37dqqsgPH
|
||||
xOQ3KQNxnTteS00kzQIDAQAB
|
||||
-----END PUBLIC KEY-----`
|
||||
)
|
||||
|
||||
func TestParseKey(t *testing.T) {
|
||||
rsa, err := crypto.NewRsa(
|
||||
crypto.SHA1,
|
||||
crypto.ParsePKCS1PrivateKey([]byte(pkBytes)))
|
||||
|
||||
t.Log(rsa, err)
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := rsa.Sign(msg)
|
||||
t.Log(base64.StdEncoding.EncodeToString(sign))
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
t.Log(rsa.Verify(msg, sign))
|
||||
}
|
||||
|
||||
func TestBadPubKey(t *testing.T) {
|
||||
|
||||
context.TODO()
|
||||
rsa, err := crypto.NewRsa(
|
||||
crypto.SHA1,
|
||||
crypto.ParsePKCS1PrivateKey([]byte(pkBytes)),
|
||||
crypto.ParsePKIXPublicKey([]byte(badPubBytes)))
|
||||
|
||||
t.Log(rsa, err)
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := rsa.Sign(msg)
|
||||
t.Log(base64.StdEncoding.EncodeToString(sign))
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
t.Log(rsa.Verify(msg, sign))
|
||||
}
|
109
crypto/sm2.go
Normal file
109
crypto/sm2.go
Normal file
@ -0,0 +1,109 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
s "github.com/tjfoc/gmsm/sm2"
|
||||
x "github.com/tjfoc/gmsm/x509"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultMode = C1C3C2
|
||||
C1C3C2 = 0
|
||||
C1C2C3 = 1
|
||||
)
|
||||
|
||||
type sm2Instance struct {
|
||||
mode int
|
||||
prk *s.PrivateKey
|
||||
puk *s.PublicKey
|
||||
}
|
||||
|
||||
type option func(*sm2Instance) error
|
||||
|
||||
func NewSm2(opts ...option) (*sm2Instance, error) {
|
||||
o := &sm2Instance{
|
||||
mode: defaultMode,
|
||||
}
|
||||
|
||||
for _, f := range opts {
|
||||
if err := f(o); err != nil {
|
||||
return o, err
|
||||
}
|
||||
}
|
||||
|
||||
if o.prk == nil {
|
||||
priv, err := s.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.prk = priv
|
||||
}
|
||||
|
||||
if o.puk == nil {
|
||||
o.puk = &o.prk.PublicKey
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func ParseSm2PrivateKey(p []byte, pwd []byte) option {
|
||||
return func(so *sm2Instance) error {
|
||||
fmt.Println(string(p))
|
||||
|
||||
priv, err := x.ReadPrivateKeyFromPem(p, pwd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
so.prk = priv
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ParseSm2PublicKey(p []byte) option {
|
||||
return func(so *sm2Instance) error {
|
||||
pub, err := x.ReadPublicKeyFromPem(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
so.puk = pub
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func WithMode(mode int) option {
|
||||
return func(so *sm2Instance) error {
|
||||
so.mode = mode
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (o *sm2Instance) Encrypt(msg []byte) ([]byte, error) {
|
||||
return s.Encrypt(o.puk, msg, rand.Reader, o.mode)
|
||||
}
|
||||
|
||||
func (o *sm2Instance) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
return s.Decrypt(o.prk, cipherText, o.mode)
|
||||
}
|
||||
|
||||
func (o *sm2Instance) Sign(msg []byte) ([]byte, error) {
|
||||
if o.prk == nil {
|
||||
return []byte{}, errors.New("private key is nil")
|
||||
}
|
||||
|
||||
b, e := o.prk.Sign(rand.Reader, msg, nil)
|
||||
return b, e
|
||||
}
|
||||
|
||||
func (o *sm2Instance) Verify(msg []byte, sign []byte) bool {
|
||||
if o.puk == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return o.puk.Verify(msg, sign)
|
||||
}
|
99
crypto/sm2_test.go
Normal file
99
crypto/sm2_test.go
Normal file
@ -0,0 +1,99 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/crypto"
|
||||
)
|
||||
|
||||
func TestNewSm2(t *testing.T) {
|
||||
o, err := crypto.NewSm2()
|
||||
t.Logf("%+v, %v", o, err)
|
||||
|
||||
t.Log(crypto.NewSm2(crypto.ParseSm2PrivateKey([]byte{}, []byte{})))
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := o.Sign(msg)
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
ok := o.Verify(msg, sign)
|
||||
if !ok {
|
||||
t.Fail()
|
||||
}
|
||||
t.Log(ok)
|
||||
}
|
||||
|
||||
const (
|
||||
privPem = `-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIH8MFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAgXsd3MYu0BwwICCAAw
|
||||
DAYIKoZIhvcNAgcFADAdBglghkgBZQMEASoEEJzb8/1Aqhbv2cf777VoW0cEgaAz
|
||||
DbRJgs76YYpya9wiaZeAavSn8Ydi+CYSvvQurqa1q0Hmna/Lgcgt2Z0F3fFN/EYP
|
||||
wmDCd6SQ5hdPfQLBtkpDQdFylIHAm26O0smciB7NlfWSdgIluFacbMJ++/YHvcDp
|
||||
yl1qcRpjk+s+1+8YBUp7Mp1CXbDXdQebH9xezOE3OH8+9zO3qi5qeLEVofgRQJIY
|
||||
k8EBbLsGMy4WlSr0u29A
|
||||
-----END ENCRYPTED PRIVATE KEY-----`
|
||||
|
||||
pubPem = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEvfHGxZL/wzWLYgPsHEpFxCCwXKSr
|
||||
XExvTJS6FAem+lQTyHwOGT+qFf67J77d5y/exn6E5br79nsJkoM/7A72nQ==
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
badPubPem = `-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE3Og1rzeSs2wO9+YFIdgnAES03u1n
|
||||
hslcifiQY8173nHtaB3R6T0PwRQTwKbpdec0dwVCpvVcdzHtivndlG0mqQ==
|
||||
-----END PUBLIC KEY-----`
|
||||
)
|
||||
|
||||
func TestPrivatePem(t *testing.T) {
|
||||
signer, err := crypto.NewSm2(
|
||||
crypto.ParseSm2PrivateKey([]byte(privPem), []byte{}),
|
||||
crypto.ParseSm2PublicKey([]byte(pubPem)))
|
||||
|
||||
t.Log(signer, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := signer.Sign(msg)
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
t.Log(signer.Verify(msg, sign))
|
||||
}
|
||||
|
||||
func TestBadPublicPem(t *testing.T) {
|
||||
signer, err := crypto.NewSm2(
|
||||
crypto.ParseSm2PrivateKey([]byte(privPem), []byte{}),
|
||||
crypto.ParseSm2PublicKey([]byte(badPubPem)))
|
||||
|
||||
t.Log(signer, err)
|
||||
|
||||
msg := []byte("123456")
|
||||
sign, err := signer.Sign(msg)
|
||||
t.Log(hex.EncodeToString(sign), err)
|
||||
|
||||
t.Log(signer.Verify(msg, sign))
|
||||
}
|
||||
|
||||
const pemString = `-----BEGIN EC PARAMETERS-----
|
||||
BggqgRzPVQGCLQ==
|
||||
-----END EC PARAMETERS-----
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIAU/RPiFOw8sI+4dM/0ZusJ7dWxi72DpnOukgGNZfPP5oAoGCCqBHM9V
|
||||
AYItoUQDQgAEbl5hPO00SJnkTpNjefes6QjmOrhQTrcocBQ0V9yB3ow/COroyHIp
|
||||
MV8UROLaT5kNUim8Z6XQjL+TWrfo11JQ2w==
|
||||
-----END EC PRIVATE KEY-----`
|
||||
|
||||
func TestDecodePem(t *testing.T) {
|
||||
|
||||
block, _ := pem.Decode([]byte(pemString))
|
||||
fmt.Println(string(block.Bytes))
|
||||
|
||||
prv, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
t.Log(prv, err)
|
||||
}
|
81
crypto/sm4.go
Normal file
81
crypto/sm4.go
Normal file
@ -0,0 +1,81 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/tjfoc/gmsm/sm4"
|
||||
)
|
||||
|
||||
type sm4Instance struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func Sm4(key []byte) *sm4Instance {
|
||||
return &sm4Instance{key: key}
|
||||
}
|
||||
|
||||
type sm4EcbInstance struct {
|
||||
*sm4Instance
|
||||
}
|
||||
|
||||
func (o *sm4Instance) ECB() *sm4EcbInstance {
|
||||
return &sm4EcbInstance{
|
||||
sm4Instance: o,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *sm4EcbInstance) Encrypt(msg []byte) ([]byte, error) {
|
||||
return sm4.Sm4Ecb(o.key, msg, true)
|
||||
}
|
||||
|
||||
func (o *sm4EcbInstance) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
return sm4.Sm4Ecb(o.key, cipherText, false)
|
||||
}
|
||||
|
||||
type sm4CbcInstance struct {
|
||||
*sm4Instance
|
||||
iv []byte
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (o *sm4Instance) CBC() *sm4CbcInstance {
|
||||
return &sm4CbcInstance{
|
||||
sm4Instance: o,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *sm4CbcInstance) WithIV(iv []byte) *sm4CbcInstance {
|
||||
o.iv = iv
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *sm4CbcInstance) Encrypt(msg []byte) ([]byte, error) {
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
|
||||
if err := sm4.SetIV(o.iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resetIV()
|
||||
|
||||
return sm4.Sm4Cbc(o.key, msg, true)
|
||||
}
|
||||
|
||||
func (o *sm4CbcInstance) Decrypt(cipherText []byte) ([]byte, error) {
|
||||
o.lock.Lock()
|
||||
defer o.lock.Unlock()
|
||||
|
||||
if err := sm4.SetIV(o.iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resetIV()
|
||||
|
||||
return sm4.Sm4Cbc(o.key, cipherText, false)
|
||||
}
|
||||
|
||||
var emptyIV = make([]byte, sm4.BlockSize)
|
||||
|
||||
func resetIV() {
|
||||
_ = sm4.SetIV(emptyIV)
|
||||
}
|
64
crypto/sm4_test.go
Normal file
64
crypto/sm4_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/charlienet/go-mixed/crypto"
|
||||
"github.com/tjfoc/gmsm/sm4"
|
||||
)
|
||||
|
||||
func TestGmsmSm4(t *testing.T) {
|
||||
key, _ := hex.DecodeString("0123456789ABCDEFFEDCBA9876543210")
|
||||
msg, _ := hex.DecodeString("F0A2B07E64DD2C2590F93E4EDD90FBB4")
|
||||
|
||||
c, err := sm4.Sm4Ecb(key, msg, true)
|
||||
t.Log(hex.EncodeToString(c), err)
|
||||
|
||||
d, err := sm4.Sm4Ecb(key, c, false)
|
||||
t.Log(hex.EncodeToString(d), err)
|
||||
}
|
||||
|
||||
func TestPadding(t *testing.T) {
|
||||
msg, _ := hex.DecodeString("F0A2B07E64DD2C2590F93E4EDD90FBB4")
|
||||
|
||||
blockSize := sm4.BlockSize
|
||||
padding := blockSize - len(msg)%blockSize
|
||||
t.Log(padding)
|
||||
}
|
||||
|
||||
func TestSm4(t *testing.T) {
|
||||
key := []byte("1234567890abcdef")
|
||||
msg := []byte("123321123321123321123321123321")
|
||||
|
||||
cipherText, err := crypto.Sm4(key).ECB().Encrypt(msg)
|
||||
t.Log("ECB加密:", hex.EncodeToString(cipherText), err)
|
||||
|
||||
de, err := crypto.Sm4(key).ECB().Decrypt(cipherText)
|
||||
t.Log("ECB解密:", string(de), err)
|
||||
|
||||
cipherText, err = crypto.Sm4(key).CBC().WithIV(key).Encrypt(msg)
|
||||
t.Log("CBC加密:", hex.EncodeToString(cipherText), err)
|
||||
|
||||
de, err = crypto.Sm4(key).CBC().WithIV(key).Decrypt(cipherText)
|
||||
t.Log("CBC解密:", string(de), err)
|
||||
}
|
||||
|
||||
func TestDecrypt(t *testing.T) {
|
||||
key := []byte("XbBpuLSzaXtlOYFV")
|
||||
iv := []byte("UISwD9fW6cFh9SNS")
|
||||
en := "BAD4C05DB0A51895A38D976F97057C2D1743473CE6DABC3456DD4EA751A9794D81096050DBA084F1CB3791C63DFFEDD1D63B046B155FD06386DEE8434A20D8A7465780EF3660ED1073A253DEA4768AB735E2DDEB4602927D3FF85E429C9B7557E6A3A198F4781642CDD30449968FBD2E54E0425E327805DFB0A1DA4FAE33AC68A3377D20042A9459EEF09BEE8CBE483BF61D32B7BB402730AA2276EA3C3A078B895D684A91DD7EEF0F7A25289B1D4905AF524126E8C3DBCB0AB73C92ABC1A83ECA687777B9B609DD8B0F69602EC3E74243E00B33D51EDF930A5316BCB388E4B7B2A6EFDD8B0BE4A19625D297B25D2BD2E5424F2E9B6A4BBF6A70DBE3C6ABB635554AC21CE053D7ECA23D82EF8060C874D507FC27CFCC06EDF41AF98ED0C2C59E39146CC28BA7630D74870BD372863FC4"
|
||||
|
||||
c := crypto.Sm4(key).CBC().WithIV(iv)
|
||||
|
||||
b, _ := hex.DecodeString(en)
|
||||
decrypted, err := c.Decrypt(b)
|
||||
t.Log(hex.EncodeToString(sm4.IV))
|
||||
t.Log(hex.EncodeToString(decrypted))
|
||||
t.Log(string(decrypted), err)
|
||||
|
||||
encrypted, err := c.Encrypt(decrypted)
|
||||
t.Log(err)
|
||||
ddd, err := c.Decrypt(encrypted)
|
||||
t.Log(string(ddd), err)
|
||||
}
|
42
crypto/utils.go
Normal file
42
crypto/utils.go
Normal file
@ -0,0 +1,42 @@
|
||||
package crypto
|
||||
|
||||
import "crypto"
|
||||
|
||||
type Hash uint
|
||||
|
||||
const (
|
||||
MD4 Hash = 1 + iota // import golang.org/x/crypto/md4
|
||||
MD5 // import crypto/md5
|
||||
SHA1 // import crypto/sha1
|
||||
SHA224 // import crypto/sha256
|
||||
SHA256 // import crypto/sha256
|
||||
SHA384 // import crypto/sha512
|
||||
SHA512 // import crypto/sha512
|
||||
MD5SHA1 // no implementation; MD5+SHA1 used for TLS RSA
|
||||
RIPEMD160 // import golang.org/x/crypto/ripemd160
|
||||
SHA3_224 // import golang.org/x/crypto/sha3
|
||||
SHA3_256 // import golang.org/x/crypto/sha3
|
||||
SHA3_384 // import golang.org/x/crypto/sha3
|
||||
SHA3_512 // import golang.org/x/crypto/sha3
|
||||
SHA512_224 // import crypto/sha512
|
||||
SHA512_256 // import crypto/sha512
|
||||
BLAKE2s_256 // import golang.org/x/crypto/blake2s
|
||||
BLAKE2b_256 // import golang.org/x/crypto/blake2b
|
||||
BLAKE2b_384 // import golang.org/x/crypto/blake2b
|
||||
BLAKE2b_512 // import golang.org/x/crypto/blake2b
|
||||
SM3 // import
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRsaBits = 1024
|
||||
)
|
||||
|
||||
type hashOptions struct {
|
||||
h crypto.Hash
|
||||
}
|
||||
|
||||
func (o *hashOptions) getHash(msg []byte) []byte {
|
||||
h := o.h.New()
|
||||
h.Write(msg)
|
||||
return h.Sum(nil)
|
||||
}
|
Reference in New Issue
Block a user