diff --git a/cgo_go122.go b/cgo_go122.go index 555f58c5..17ad0afb 100644 --- a/cgo_go122.go +++ b/cgo_go122.go @@ -9,5 +9,7 @@ package openssl // functions that are known to allocate. #cgo noescape go_openssl_EVP_PKEY_derive #cgo nocallback go_openssl_EVP_PKEY_derive +#cgo noescape go_openssl_EVP_EncryptUpdate +#cgo nocallback go_openssl_EVP_EncryptUpdate */ import "C" diff --git a/cipher.go b/cipher.go index df6c40f1..b32a3c58 100644 --- a/cipher.go +++ b/cipher.go @@ -22,6 +22,7 @@ const ( cipherAES256 cipherDES cipherDES3 + cipherRC4 ) func (c cipherKind) String() string { @@ -36,6 +37,8 @@ func (c cipherKind) String() string { return "DES" case cipherDES3: return "DES3" + case cipherRC4: + return "RC4" default: panic("unknown cipher kind: " + strconv.Itoa(int(c))) } @@ -44,7 +47,8 @@ func (c cipherKind) String() string { type cipherMode int8 const ( - cipherModeECB cipherMode = iota + cipherModeNone cipherMode = -1 + cipherModeECB cipherMode = iota cipherModeCBC cipherModeCTR cipherModeGCM @@ -133,6 +137,8 @@ func loadCipher(k cipherKind, mode cipherMode) (cipher C.GO_EVP_CIPHER_PTR) { case cipherModeCBC: cipher = C.go_openssl_EVP_des_ede3_cbc() } + case cipherRC4: + cipher = C.go_openssl_EVP_rc4() } return cipher } @@ -477,17 +483,34 @@ func sliceForAppend(in []byte, n int) (head, tail []byte) { return } -func newCipherCtx(kind cipherKind, mode cipherMode, encrypt cipherOp, key, iv []byte) (C.GO_EVP_CIPHER_CTX_PTR, error) { +func newCipherCtx(kind cipherKind, mode cipherMode, encrypt cipherOp, key, iv []byte) (ctx C.GO_EVP_CIPHER_CTX_PTR, err error) { cipher := loadCipher(kind, mode) if cipher == nil { panic("crypto/cipher: unsupported cipher: " + kind.String()) } - ctx := C.go_openssl_EVP_CIPHER_CTX_new() + ctx = C.go_openssl_EVP_CIPHER_CTX_new() if ctx == nil { return nil, fail("unable to create EVP cipher ctx") } + defer func() { + if err != nil { + C.go_openssl_EVP_CIPHER_CTX_free(ctx) + } + }() + if kind == cipherRC4 { + // RC4 cipher supports a variable key length. + // We need to set the key length before setting the key, + // and to do so we need to have an initialized cipher ctx. + if C.go_openssl_EVP_CipherInit_ex(ctx, cipher, nil, nil, nil, C.int(encrypt)) != 1 { + return nil, newOpenSSLError("EVP_CipherInit_ex") + } + if C.go_openssl_EVP_CIPHER_CTX_set_key_length(ctx, C.int(len(key))) != 1 { + return nil, newOpenSSLError("EVP_CIPHER_CTX_set_key_length") + } + // Pass nil to the next call to EVP_CipherInit_ex to avoid resetting ctx's cipher. + cipher = nil + } if C.go_openssl_EVP_CipherInit_ex(ctx, cipher, nil, base(key), base(iv), C.int(encrypt)) != 1 { - C.go_openssl_EVP_CIPHER_CTX_free(ctx) return nil, fail("unable to initialize EVP cipher ctx") } return ctx, nil diff --git a/rc4.go b/rc4.go new file mode 100644 index 00000000..db28b61f --- /dev/null +++ b/rc4.go @@ -0,0 +1,63 @@ +//go:build !cmd_go_bootstrap + +package openssl + +// #include "goopenssl.h" +import "C" +import "runtime" + +// SupportsRC4 returns true if NewRC4Cipher is supported. +func SupportsRC4() bool { + // True for stock OpenSSL 1. + // False for stock OpenSSL 3 unless the legacy provider is available. + return loadCipher(cipherRC4, cipherModeNone) != nil +} + +// A RC4Cipher is an instance of RC4 using a particular key. +type RC4Cipher struct { + ctx C.GO_EVP_CIPHER_CTX_PTR +} + +// NewRC4Cipher creates and returns a new Cipher. +func NewRC4Cipher(key []byte) (*RC4Cipher, error) { + ctx, err := newCipherCtx(cipherRC4, cipherModeNone, cipherOpEncrypt, key, nil) + if err != nil { + return nil, err + } + c := &RC4Cipher{ctx} + runtime.SetFinalizer(c, (*RC4Cipher).finalize) + return c, nil +} + +func (c *RC4Cipher) finalize() { + if c.ctx != nil { + C.go_openssl_EVP_CIPHER_CTX_free(c.ctx) + } +} + +// Reset zeros the key data and makes the Cipher unusable. +func (c *RC4Cipher) Reset() { + if c.ctx != nil { + C.go_openssl_EVP_CIPHER_CTX_free(c.ctx) + c.ctx = nil + } +} + +// XORKeyStream sets dst to the result of XORing src with the key stream. +// Dst and src must overlap entirely or not at all. +func (c *RC4Cipher) XORKeyStream(dst, src []byte) { + if c.ctx == nil || len(src) == 0 { + return + } + if inexactOverlap(dst[:len(src)], src) { + panic("crypto/rc4: invalid buffer overlap") + } + var outLen C.int + if C.go_openssl_EVP_EncryptUpdate(c.ctx, base(dst), &outLen, base(src), C.int(len(src))) != 1 { + panic("crypto/cipher: EncryptUpdate failed") + } + if int(outLen) != len(src) { + panic("crypto/rc4: src not fully XORed") + } + runtime.KeepAlive(c) +} diff --git a/rc4_test.go b/rc4_test.go new file mode 100644 index 00000000..e9f8dca9 --- /dev/null +++ b/rc4_test.go @@ -0,0 +1,167 @@ +package openssl_test + +import ( + "bytes" + "fmt" + "testing" + + "github.com/golang-fips/openssl/v2" +) + +type rc4Test struct { + key, keystream []byte +} + +var golden = []rc4Test{ + // Test vectors from the original cypherpunk posting of ARC4: + // https://groups.google.com/group/sci.crypt/msg/10a300c9d21afca0?pli=1 + { + []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, + []byte{0x74, 0x94, 0xc2, 0xe7, 0x10, 0x4b, 0x08, 0x79}, + }, + { + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{0xde, 0x18, 0x89, 0x41, 0xa3, 0x37, 0x5d, 0x3a}, + }, + { + []byte{0xef, 0x01, 0x23, 0x45}, + []byte{0xd6, 0xa1, 0x41, 0xa7, 0xec, 0x3c, 0x38, 0xdf, 0xbd, 0x61}, + }, + + // Test vectors from the Wikipedia page: https://en.wikipedia.org/wiki/RC4 + { + []byte{0x4b, 0x65, 0x79}, + []byte{0xeb, 0x9f, 0x77, 0x81, 0xb7, 0x34, 0xca, 0x72, 0xa7, 0x19}, + }, + { + []byte{0x57, 0x69, 0x6b, 0x69}, + []byte{0x60, 0x44, 0xdb, 0x6d, 0x41, 0xb7}, + }, + { + []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + []byte{ + 0xde, 0x18, 0x89, 0x41, 0xa3, 0x37, 0x5d, 0x3a, + 0x8a, 0x06, 0x1e, 0x67, 0x57, 0x6e, 0x92, 0x6d, + 0xc7, 0x1a, 0x7f, 0xa3, 0xf0, 0xcc, 0xeb, 0x97, + 0x45, 0x2b, 0x4d, 0x32, 0x27, 0x96, 0x5f, 0x9e, + 0xa8, 0xcc, 0x75, 0x07, 0x6d, 0x9f, 0xb9, 0xc5, + 0x41, 0x7a, 0xa5, 0xcb, 0x30, 0xfc, 0x22, 0x19, + 0x8b, 0x34, 0x98, 0x2d, 0xbb, 0x62, 0x9e, 0xc0, + 0x4b, 0x4f, 0x8b, 0x05, 0xa0, 0x71, 0x08, 0x50, + 0x92, 0xa0, 0xc3, 0x58, 0x4a, 0x48, 0xe4, 0xa3, + 0x0a, 0x39, 0x7b, 0x8a, 0xcd, 0x1d, 0x00, 0x9e, + 0xc8, 0x7d, 0x68, 0x11, 0xf2, 0x2c, 0xf4, 0x9c, + 0xa3, 0xe5, 0x93, 0x54, 0xb9, 0x45, 0x15, 0x35, + 0xa2, 0x18, 0x7a, 0x86, 0x42, 0x6c, 0xca, 0x7d, + 0x5e, 0x82, 0x3e, 0xba, 0x00, 0x44, 0x12, 0x67, + 0x12, 0x57, 0xb8, 0xd8, 0x60, 0xae, 0x4c, 0xbd, + 0x4c, 0x49, 0x06, 0xbb, 0xc5, 0x35, 0xef, 0xe1, + 0x58, 0x7f, 0x08, 0xdb, 0x33, 0x95, 0x5c, 0xdb, + 0xcb, 0xad, 0x9b, 0x10, 0xf5, 0x3f, 0xc4, 0xe5, + 0x2c, 0x59, 0x15, 0x65, 0x51, 0x84, 0x87, 0xfe, + 0x08, 0x4d, 0x0e, 0x3f, 0x03, 0xde, 0xbc, 0xc9, + 0xda, 0x1c, 0xe9, 0x0d, 0x08, 0x5c, 0x2d, 0x8a, + 0x19, 0xd8, 0x37, 0x30, 0x86, 0x16, 0x36, 0x92, + 0x14, 0x2b, 0xd8, 0xfc, 0x5d, 0x7a, 0x73, 0x49, + 0x6a, 0x8e, 0x59, 0xee, 0x7e, 0xcf, 0x6b, 0x94, + 0x06, 0x63, 0xf4, 0xa6, 0xbe, 0xe6, 0x5b, 0xd2, + 0xc8, 0x5c, 0x46, 0x98, 0x6c, 0x1b, 0xef, 0x34, + 0x90, 0xd3, 0x7b, 0x38, 0xda, 0x85, 0xd3, 0x2e, + 0x97, 0x39, 0xcb, 0x23, 0x4a, 0x2b, 0xe7, 0x40, + }, + }, +} + +func TestRC4Golden(t *testing.T) { + if !openssl.SupportsRC4() { + t.Skip("RC4 is not supported") + } + for gi, g := range golden { + data := make([]byte, len(g.keystream)) + for i := range data { + data[i] = byte(i) + } + + expect := make([]byte, len(g.keystream)) + for i := range expect { + expect[i] = byte(i) ^ g.keystream[i] + } + + for size := 1; size <= len(g.keystream); size++ { + c, err := openssl.NewRC4Cipher(g.key) + if err != nil { + t.Fatalf("#%d: NewCipher: %v", gi, err) + } + + off := 0 + for off < len(g.keystream) { + n := len(g.keystream) - off + if n > size { + n = size + } + desc := fmt.Sprintf("#%d@[%d:%d]", gi, off, off+n) + src := data[off : off+n] + expect := expect[off : off+n] + dst := make([]byte, len(src)) + c.XORKeyStream(dst, src) + for i, v := range dst { + if v != expect[i] { + t.Fatalf("%s: mismatch at byte %d:\nhave %x\nwant %x", desc, i, dst, expect) + } + } + off += n + } + } + } +} + +func TestRC4Block(t *testing.T) { + if !openssl.SupportsRC4() { + t.Skip("RC4 is not supported") + } + c1a, _ := openssl.NewRC4Cipher(golden[0].key) + c1b, _ := openssl.NewRC4Cipher(golden[1].key) + data1 := make([]byte, 1<<20) + for i := range data1 { + c1a.XORKeyStream(data1[i:i+1], data1[i:i+1]) + c1b.XORKeyStream(data1[i:i+1], data1[i:i+1]) + } + + c2a, _ := openssl.NewRC4Cipher(golden[0].key) + c2b, _ := openssl.NewRC4Cipher(golden[1].key) + data2 := make([]byte, 1<<20) + c2a.XORKeyStream(data2, data2) + c2b.XORKeyStream(data2, data2) + + if !bytes.Equal(data1, data2) { + t.Fatalf("bad block") + } +} + +func benchmarkRC4(b *testing.B, size int64) { + if !openssl.SupportsRC4() { + b.Skip("RC4 is not supported") + } + buf := make([]byte, size) + c, err := openssl.NewRC4Cipher(golden[0].key) + if err != nil { + panic(err) + } + b.SetBytes(size) + + for i := 0; i < b.N; i++ { + c.XORKeyStream(buf, buf) + } +} + +func BenchmarkRC4_128(b *testing.B) { + benchmarkRC4(b, 128) +} + +func BenchmarkRC4_1K(b *testing.B) { + benchmarkRC4(b, 1024) +} + +func BenchmarkRC4_8K(b *testing.B) { + benchmarkRC4(b, 8096) +} diff --git a/shims.h b/shims.h index 858c47e7..071bf706 100644 --- a/shims.h +++ b/shims.h @@ -257,7 +257,9 @@ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ecb, (void), ()) \ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_cbc, (void), ()) \ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ede3_ecb, (void), ()) \ DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_des_ede3_cbc, (void), ()) \ +DEFINEFUNC(const GO_EVP_CIPHER_PTR, EVP_rc4, (void), ()) \ DEFINEFUNC_RENAMED_3_0(int, EVP_CIPHER_get_block_size, EVP_CIPHER_block_size, (const GO_EVP_CIPHER_PTR cipher), (cipher)) \ +DEFINEFUNC(int, EVP_CIPHER_CTX_set_key_length, (GO_EVP_CIPHER_CTX_PTR x, int keylen), (x, keylen)) \ DEFINEFUNC(void, EVP_CIPHER_CTX_free, (GO_EVP_CIPHER_CTX_PTR arg0), (arg0)) \ DEFINEFUNC(int, EVP_CIPHER_CTX_ctrl, (GO_EVP_CIPHER_CTX_PTR ctx, int type, int arg, void *ptr), (ctx, type, arg, ptr)) \ DEFINEFUNC(GO_EVP_PKEY_PTR, EVP_PKEY_new, (void), ()) \