Initial QSfera import
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "aescbc",
|
||||
srcs = ["aescbc.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = ["//internal/pool"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "aescbc_test",
|
||||
srcs = ["aescbc_test.go"],
|
||||
embed = [":aescbc"],
|
||||
deps = ["@com_github_stretchr_testify//require"]
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":aescbc",
|
||||
visibility = ["//jwe:__subpackages__"],
|
||||
)
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
package aescbc
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
)
|
||||
|
||||
const (
|
||||
NonceSize = 16
|
||||
)
|
||||
|
||||
const defaultBufSize int64 = 256 * 1024 * 1024
|
||||
|
||||
var maxBufSize atomic.Int64
|
||||
|
||||
func init() {
|
||||
SetMaxBufferSize(defaultBufSize)
|
||||
}
|
||||
|
||||
func SetMaxBufferSize(siz int64) {
|
||||
if siz <= 0 {
|
||||
siz = defaultBufSize
|
||||
}
|
||||
maxBufSize.Store(siz)
|
||||
}
|
||||
|
||||
func pad(buf []byte, n int) []byte {
|
||||
rem := n - len(buf)%n
|
||||
if rem == 0 {
|
||||
return buf
|
||||
}
|
||||
|
||||
bufsiz := len(buf) + rem
|
||||
mbs := maxBufSize.Load()
|
||||
if int64(bufsiz) > mbs {
|
||||
panic(fmt.Errorf("failed to allocate buffer"))
|
||||
}
|
||||
newbuf := make([]byte, bufsiz)
|
||||
copy(newbuf, buf)
|
||||
|
||||
for i := len(buf); i < len(newbuf); i++ {
|
||||
newbuf[i] = byte(rem)
|
||||
}
|
||||
return newbuf
|
||||
}
|
||||
|
||||
// ref. https://github.com/golang/go/blob/c3db64c0f45e8f2d75c5b59401e0fc925701b6f4/src/crypto/tls/conn.go#L279-L324
|
||||
//
|
||||
// extractPadding returns, in constant time, the length of the padding to remove
|
||||
// from the end of payload. It also returns a byte which is equal to 255 if the
|
||||
// padding was valid and 0 otherwise. See RFC 2246, Section 6.2.3.2.
|
||||
func extractPadding(payload []byte) (toRemove int, good byte) {
|
||||
if len(payload) < 1 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
paddingLen := payload[len(payload)-1]
|
||||
t := uint(len(payload)) - uint(paddingLen)
|
||||
// if len(payload) > paddingLen then the MSB of t is zero
|
||||
good = byte(int32(^t) >> 31)
|
||||
|
||||
// The maximum possible padding length plus the actual length field
|
||||
toCheck := 256
|
||||
// The length of the padded data is public, so we can use an if here
|
||||
toCheck = min(toCheck, len(payload))
|
||||
|
||||
for i := 1; i <= toCheck; i++ {
|
||||
t := uint(paddingLen) - uint(i)
|
||||
// if i <= paddingLen then the MSB of t is zero
|
||||
mask := byte(int32(^t) >> 31)
|
||||
b := payload[len(payload)-i]
|
||||
good &^= mask&paddingLen ^ mask&b
|
||||
}
|
||||
|
||||
// We AND together the bits of good and replicate the result across
|
||||
// all the bits.
|
||||
good &= good << 4
|
||||
good &= good << 2
|
||||
good &= good << 1
|
||||
good = uint8(int8(good) >> 7)
|
||||
|
||||
// Zero the padding length on error. This ensures any unchecked bytes
|
||||
// are included in the MAC. Otherwise, an attacker that could
|
||||
// distinguish MAC failures from padding failures could mount an attack
|
||||
// similar to POODLE in SSL 3.0: given a good ciphertext that uses a
|
||||
// full block's worth of padding, replace the final block with another
|
||||
// block. If the MAC check passed but the padding check failed, the
|
||||
// last byte of that block decrypted to the block size.
|
||||
//
|
||||
// See also macAndPaddingGood logic below.
|
||||
paddingLen &= good
|
||||
|
||||
toRemove = int(paddingLen)
|
||||
return
|
||||
}
|
||||
|
||||
type Hmac struct {
|
||||
blockCipher cipher.Block
|
||||
hash func() hash.Hash
|
||||
keysize int
|
||||
tagsize int
|
||||
integrityKey []byte
|
||||
}
|
||||
|
||||
type BlockCipherFunc func([]byte) (cipher.Block, error)
|
||||
|
||||
func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) {
|
||||
keysize := len(key) / 2
|
||||
ikey := key[:keysize]
|
||||
ekey := key[keysize:]
|
||||
|
||||
bc, ciphererr := f(ekey)
|
||||
if ciphererr != nil {
|
||||
err = fmt.Errorf(`failed to execute block cipher function: %w`, ciphererr)
|
||||
return
|
||||
}
|
||||
|
||||
var hfunc func() hash.Hash
|
||||
switch keysize {
|
||||
case 16:
|
||||
hfunc = sha256.New
|
||||
case 24:
|
||||
hfunc = sha512.New384
|
||||
case 32:
|
||||
hfunc = sha512.New
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key size %d", keysize)
|
||||
}
|
||||
|
||||
return &Hmac{
|
||||
blockCipher: bc,
|
||||
hash: hfunc,
|
||||
integrityKey: ikey,
|
||||
keysize: keysize,
|
||||
tagsize: keysize, // NonceSize,
|
||||
// While investigating GH #207, I stumbled upon another problem where
|
||||
// the computed tags don't match on decrypt. After poking through the
|
||||
// code using a bunch of debug statements, I've finally found out that
|
||||
// tagsize = keysize makes the whole thing work.
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NonceSize fulfills the crypto.AEAD interface
|
||||
func (c Hmac) NonceSize() int {
|
||||
return NonceSize
|
||||
}
|
||||
|
||||
// Overhead fulfills the crypto.AEAD interface
|
||||
func (c Hmac) Overhead() int {
|
||||
return c.blockCipher.BlockSize() + c.tagsize
|
||||
}
|
||||
|
||||
func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) {
|
||||
var buf [8]byte
|
||||
binary.BigEndian.PutUint64(buf[:], uint64(len(aad)*8))
|
||||
|
||||
h := hmac.New(c.hash, c.integrityKey)
|
||||
|
||||
// compute the tag
|
||||
// no need to check errors because Write never returns an error: https://pkg.go.dev/hash#Hash
|
||||
//
|
||||
// > Write (via the embedded io.Writer interface) adds more data to the running hash.
|
||||
// > It never returns an error.
|
||||
h.Write(aad)
|
||||
h.Write(nonce)
|
||||
h.Write(ciphertext)
|
||||
h.Write(buf[:])
|
||||
s := h.Sum(nil)
|
||||
return s[:c.tagsize], nil
|
||||
}
|
||||
|
||||
func ensureSize(dst []byte, n int) []byte {
|
||||
// if the dst buffer has enough length just copy the relevant parts to it.
|
||||
// Otherwise create a new slice that's big enough, and operate on that
|
||||
// Note: I think go-jose has a bug in that it checks for cap(), but not len().
|
||||
ret := dst
|
||||
if diff := n - len(dst); diff > 0 {
|
||||
// dst is not big enough
|
||||
ret = make([]byte, n)
|
||||
copy(ret, dst)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Seal fulfills the crypto.AEAD interface
|
||||
func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte {
|
||||
ctlen := len(plaintext)
|
||||
bufsiz := ctlen + c.Overhead()
|
||||
mbs := maxBufSize.Load()
|
||||
|
||||
if int64(bufsiz) > mbs {
|
||||
panic(fmt.Errorf("failed to allocate buffer"))
|
||||
}
|
||||
ciphertext := make([]byte, bufsiz)[:ctlen]
|
||||
copy(ciphertext, plaintext)
|
||||
ciphertext = pad(ciphertext, c.blockCipher.BlockSize())
|
||||
|
||||
cbc := cipher.NewCBCEncrypter(c.blockCipher, nonce)
|
||||
cbc.CryptBlocks(ciphertext, ciphertext)
|
||||
|
||||
authtag, err := c.ComputeAuthTag(data, nonce, ciphertext)
|
||||
if err != nil {
|
||||
// Hmac implements cipher.AEAD interface. Seal can't return error.
|
||||
// But currently it never reach here because of Hmac.ComputeAuthTag doesn't return error.
|
||||
panic(fmt.Errorf("failed to seal on hmac: %v", err))
|
||||
}
|
||||
|
||||
retlen := len(dst) + len(ciphertext) + len(authtag)
|
||||
|
||||
ret := ensureSize(dst, retlen)
|
||||
out := ret[len(dst):]
|
||||
n := copy(out, ciphertext)
|
||||
copy(out[n:], authtag)
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Open fulfills the crypto.AEAD interface
|
||||
func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
|
||||
if len(ciphertext) < c.keysize {
|
||||
return nil, fmt.Errorf(`invalid ciphertext (too short)`)
|
||||
}
|
||||
|
||||
tagOffset := len(ciphertext) - c.tagsize
|
||||
if tagOffset%c.blockCipher.BlockSize() != 0 {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid ciphertext (invalid length: %d %% %d != 0)",
|
||||
tagOffset,
|
||||
c.blockCipher.BlockSize(),
|
||||
)
|
||||
}
|
||||
tag := ciphertext[tagOffset:]
|
||||
ciphertext = ciphertext[:tagOffset]
|
||||
|
||||
expectedTag, err := c.ComputeAuthTag(data, nonce, ciphertext[:tagOffset])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to compute auth tag: %w`, err)
|
||||
}
|
||||
|
||||
cbc := cipher.NewCBCDecrypter(c.blockCipher, nonce)
|
||||
buf := pool.ByteSlice().GetCapacity(tagOffset)
|
||||
defer pool.ByteSlice().Put(buf)
|
||||
buf = buf[:tagOffset]
|
||||
|
||||
cbc.CryptBlocks(buf, ciphertext)
|
||||
|
||||
toRemove, good := extractPadding(buf)
|
||||
cmp := subtle.ConstantTimeCompare(expectedTag, tag) & int(good)
|
||||
if cmp != 1 {
|
||||
return nil, errors.New(`invalid ciphertext`)
|
||||
}
|
||||
|
||||
plaintext := buf[:len(buf)-toRemove]
|
||||
ret := ensureSize(dst, len(plaintext))
|
||||
out := ret[len(dst):]
|
||||
copy(out, plaintext)
|
||||
return ret, nil
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "cipher",
|
||||
srcs = [
|
||||
"cipher.go",
|
||||
"interface.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/cipher",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//jwa",
|
||||
"//jwe/internal/aescbc",
|
||||
"//jwe/internal/keygen",
|
||||
"//internal/tokens",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "cipher_test",
|
||||
srcs = ["cipher_test.go"],
|
||||
deps = [
|
||||
":cipher",
|
||||
"//jwa",
|
||||
"//internal/tokens",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":cipher",
|
||||
visibility = ["//jwe:__subpackages__"],
|
||||
)
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
package cipher
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe/internal/keygen"
|
||||
)
|
||||
|
||||
var gcm = &gcmFetcher{}
|
||||
var cbc = &cbcFetcher{}
|
||||
|
||||
func (f gcmFetcher) Fetch(key []byte, size int) (cipher.AEAD, error) {
|
||||
if len(key) != size {
|
||||
return nil, fmt.Errorf(`key size (%d) does not match expected key size (%d)`, len(key), size)
|
||||
}
|
||||
aescipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`cipher: failed to create AES cipher for GCM: %w`, err)
|
||||
}
|
||||
|
||||
aead, err := cipher.NewGCM(aescipher)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to create GCM for cipher: %w`, err)
|
||||
}
|
||||
return aead, nil
|
||||
}
|
||||
|
||||
func (f cbcFetcher) Fetch(key []byte, size int) (cipher.AEAD, error) {
|
||||
if len(key) != size {
|
||||
return nil, fmt.Errorf(`key size (%d) does not match expected key size (%d)`, len(key), size)
|
||||
}
|
||||
aead, err := aescbc.New(key, aes.NewCipher)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`cipher: failed to create AES cipher for CBC: %w`, err)
|
||||
}
|
||||
return aead, nil
|
||||
}
|
||||
|
||||
func (c AesContentCipher) KeySize() int {
|
||||
return c.keysize
|
||||
}
|
||||
|
||||
func (c AesContentCipher) TagSize() int {
|
||||
return c.tagsize
|
||||
}
|
||||
|
||||
func NewAES(alg string) (*AesContentCipher, error) {
|
||||
var keysize int
|
||||
var tagsize int
|
||||
var fetcher Fetcher
|
||||
switch alg {
|
||||
case tokens.A128GCM:
|
||||
keysize = 16
|
||||
tagsize = 16
|
||||
fetcher = gcm
|
||||
case tokens.A192GCM:
|
||||
keysize = 24
|
||||
tagsize = 16
|
||||
fetcher = gcm
|
||||
case tokens.A256GCM:
|
||||
keysize = 32
|
||||
tagsize = 16
|
||||
fetcher = gcm
|
||||
case tokens.A128CBC_HS256:
|
||||
tagsize = 16
|
||||
keysize = tagsize * 2
|
||||
fetcher = cbc
|
||||
case tokens.A192CBC_HS384:
|
||||
tagsize = 24
|
||||
keysize = tagsize * 2
|
||||
fetcher = cbc
|
||||
case tokens.A256CBC_HS512:
|
||||
tagsize = 32
|
||||
keysize = tagsize * 2
|
||||
fetcher = cbc
|
||||
default:
|
||||
return nil, fmt.Errorf("failed to create AES content cipher: invalid algorithm (%s)", alg)
|
||||
}
|
||||
|
||||
return &AesContentCipher{
|
||||
keysize: keysize,
|
||||
tagsize: tagsize,
|
||||
fetch: fetcher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertxt, tag []byte, err error) {
|
||||
var aead cipher.AEAD
|
||||
aead, err = c.fetch.Fetch(cek, c.keysize)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf(`failed to fetch AEAD: %w`, err)
|
||||
}
|
||||
|
||||
// Seal may panic (argh!), so protect ourselves from that
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
switch e := e.(type) {
|
||||
case error:
|
||||
err = e
|
||||
default:
|
||||
err = fmt.Errorf("%s", e)
|
||||
}
|
||||
err = fmt.Errorf(`failed to encrypt: %w`, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if c.NonceGenerator != nil {
|
||||
iv, err = c.NonceGenerator(aead.NonceSize())
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf(`failed to generate nonce: %w`, err)
|
||||
}
|
||||
} else {
|
||||
bs, err := keygen.Random(aead.NonceSize())
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf(`failed to generate random nonce: %w`, err)
|
||||
}
|
||||
iv = bs.Bytes()
|
||||
}
|
||||
|
||||
combined := aead.Seal(nil, iv, plaintext, aad)
|
||||
tagoffset := len(combined) - c.TagSize()
|
||||
|
||||
if tagoffset < 0 {
|
||||
panic(fmt.Sprintf("tag offset is less than 0 (combined len = %d, tagsize = %d)", len(combined), c.TagSize()))
|
||||
}
|
||||
|
||||
tag = combined[tagoffset:]
|
||||
ciphertxt = make([]byte, tagoffset)
|
||||
copy(ciphertxt, combined[:tagoffset])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c AesContentCipher) Decrypt(cek, iv, ciphertxt, tag, aad []byte) (plaintext []byte, err error) {
|
||||
aead, err := c.fetch.Fetch(cek, c.keysize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to fetch AEAD data: %w`, err)
|
||||
}
|
||||
|
||||
// Open may panic (argh!), so protect ourselves from that
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
switch e := e.(type) {
|
||||
case error:
|
||||
err = e
|
||||
default:
|
||||
err = fmt.Errorf(`%s`, e)
|
||||
}
|
||||
err = fmt.Errorf(`failed to decrypt: %w`, err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
combined := make([]byte, len(ciphertxt)+len(tag))
|
||||
copy(combined, ciphertxt)
|
||||
copy(combined[len(ciphertxt):], tag)
|
||||
|
||||
buf, aeaderr := aead.Open(nil, iv, combined, aad)
|
||||
if aeaderr != nil {
|
||||
err = fmt.Errorf(`aead.Open failed: %w`, aeaderr)
|
||||
return
|
||||
}
|
||||
plaintext = buf
|
||||
return
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cipher
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
)
|
||||
|
||||
const (
|
||||
TagSize = 16
|
||||
)
|
||||
|
||||
// ContentCipher knows how to encrypt/decrypt the content given a content
|
||||
// encryption key and other data
|
||||
type ContentCipher interface {
|
||||
KeySize() int
|
||||
Encrypt(cek, aad, plaintext []byte) ([]byte, []byte, []byte, error)
|
||||
Decrypt(cek, iv, aad, ciphertext, tag []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
type Fetcher interface {
|
||||
Fetch([]byte, int) (cipher.AEAD, error)
|
||||
}
|
||||
|
||||
type gcmFetcher struct{}
|
||||
type cbcFetcher struct{}
|
||||
|
||||
// AesContentCipher represents a cipher based on AES
|
||||
type AesContentCipher struct {
|
||||
NonceGenerator func(int) ([]byte, error)
|
||||
fetch Fetcher
|
||||
keysize int
|
||||
tagsize int
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "concatkdf",
|
||||
srcs = ["concatkdf.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf",
|
||||
visibility = ["//:__subpackages__"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "concatkdf_test",
|
||||
srcs = ["concatkdf_test.go"],
|
||||
embed = [":concatkdf"],
|
||||
deps = [
|
||||
"//jwa",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":concatkdf",
|
||||
visibility = ["//jwe:__subpackages__"],
|
||||
)
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package concatkdf
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type KDF struct {
|
||||
buf []byte
|
||||
otherinfo []byte
|
||||
z []byte
|
||||
hash crypto.Hash
|
||||
}
|
||||
|
||||
func ndata(src []byte) []byte {
|
||||
buf := make([]byte, 4+len(src))
|
||||
binary.BigEndian.PutUint32(buf, uint32(len(src)))
|
||||
copy(buf[4:], src)
|
||||
return buf
|
||||
}
|
||||
|
||||
func New(hash crypto.Hash, alg, Z, apu, apv, pubinfo, privinfo []byte) *KDF {
|
||||
algbuf := ndata(alg)
|
||||
apubuf := ndata(apu)
|
||||
apvbuf := ndata(apv)
|
||||
|
||||
concat := make([]byte, len(algbuf)+len(apubuf)+len(apvbuf)+len(pubinfo)+len(privinfo))
|
||||
n := copy(concat, algbuf)
|
||||
n += copy(concat[n:], apubuf)
|
||||
n += copy(concat[n:], apvbuf)
|
||||
n += copy(concat[n:], pubinfo)
|
||||
copy(concat[n:], privinfo)
|
||||
|
||||
return &KDF{
|
||||
hash: hash,
|
||||
otherinfo: concat,
|
||||
z: Z,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KDF) Read(out []byte) (int, error) {
|
||||
var round uint32 = 1
|
||||
h := k.hash.New()
|
||||
|
||||
for len(out) > len(k.buf) {
|
||||
h.Reset()
|
||||
|
||||
if err := binary.Write(h, binary.BigEndian, round); err != nil {
|
||||
return 0, fmt.Errorf(`failed to write round using kdf: %w`, err)
|
||||
}
|
||||
if _, err := h.Write(k.z); err != nil {
|
||||
return 0, fmt.Errorf(`failed to write z using kdf: %w`, err)
|
||||
}
|
||||
if _, err := h.Write(k.otherinfo); err != nil {
|
||||
return 0, fmt.Errorf(`failed to write other info using kdf: %w`, err)
|
||||
}
|
||||
|
||||
k.buf = append(k.buf, h.Sum(nil)...)
|
||||
round++
|
||||
}
|
||||
|
||||
n := copy(out, k.buf[:len(out)])
|
||||
k.buf = k.buf[len(out):]
|
||||
return n, nil
|
||||
}
|
||||
Generated
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "content_crypt",
|
||||
srcs = [
|
||||
"content_crypt.go",
|
||||
"interface.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//jwa",
|
||||
"//jwe/internal/cipher",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":content_crypt",
|
||||
visibility = ["//jwe:__subpackages__"],
|
||||
)
|
||||
Generated
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
package content_crypt //nolint:golint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe/internal/cipher"
|
||||
)
|
||||
|
||||
func (c Generic) Algorithm() jwa.ContentEncryptionAlgorithm {
|
||||
return c.alg
|
||||
}
|
||||
|
||||
func (c Generic) Encrypt(cek, plaintext, aad []byte) ([]byte, []byte, []byte, error) {
|
||||
iv, encrypted, tag, err := c.cipher.Encrypt(cek, plaintext, aad)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf(`failed to crypt content: %w`, err)
|
||||
}
|
||||
|
||||
return iv, encrypted, tag, nil
|
||||
}
|
||||
|
||||
func (c Generic) Decrypt(cek, iv, ciphertext, tag, aad []byte) ([]byte, error) {
|
||||
return c.cipher.Decrypt(cek, iv, ciphertext, tag, aad)
|
||||
}
|
||||
|
||||
func NewGeneric(alg jwa.ContentEncryptionAlgorithm) (*Generic, error) {
|
||||
c, err := cipher.NewAES(alg.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`aes crypt: failed to create content cipher: %w`, err)
|
||||
}
|
||||
|
||||
return &Generic{
|
||||
alg: alg,
|
||||
cipher: c,
|
||||
keysize: c.KeySize(),
|
||||
tagsize: 16,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c Generic) KeySize() int {
|
||||
return c.keysize
|
||||
}
|
||||
Generated
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
package content_crypt //nolint:golint
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe/internal/cipher"
|
||||
)
|
||||
|
||||
// Generic encrypts a message by applying all the necessary
|
||||
// modifications to the keys and the contents
|
||||
type Generic struct {
|
||||
alg jwa.ContentEncryptionAlgorithm
|
||||
keysize int
|
||||
tagsize int
|
||||
cipher cipher.ContentCipher
|
||||
}
|
||||
|
||||
type Cipher interface {
|
||||
Decrypt([]byte, []byte, []byte, []byte, []byte) ([]byte, error)
|
||||
KeySize() int
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "keygen",
|
||||
srcs = [
|
||||
"interface.go",
|
||||
"keygen.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/keygen",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/ecutil",
|
||||
"//jwa",
|
||||
"//jwe/internal/concatkdf",
|
||||
"//internal/tokens",
|
||||
"//jwk",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":keygen",
|
||||
visibility = ["//jwe:__subpackages__"],
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package keygen
|
||||
|
||||
// ByteKey is a generated key that only has the key's byte buffer
|
||||
// as its instance data. If a key needs to do more, such as providing
|
||||
// values to be set in a JWE header, that key type wraps a ByteKey
|
||||
type ByteKey []byte
|
||||
|
||||
// ByteWithECPublicKey holds the EC private key that generated
|
||||
// the key along with the key itself. This is required to set the
|
||||
// proper values in the JWE headers
|
||||
type ByteWithECPublicKey struct {
|
||||
ByteKey
|
||||
|
||||
PublicKey any
|
||||
}
|
||||
|
||||
type ByteWithIVAndTag struct {
|
||||
ByteKey
|
||||
|
||||
IV []byte
|
||||
Tag []byte
|
||||
}
|
||||
|
||||
type ByteWithSaltAndCount struct {
|
||||
ByteKey
|
||||
|
||||
Salt []byte
|
||||
Count int
|
||||
}
|
||||
|
||||
// ByteSource is an interface for things that return a byte sequence.
|
||||
// This is used for KeyGenerator so that the result of computations can
|
||||
// carry more than just the generate byte sequence.
|
||||
type ByteSource interface {
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
type Setter interface {
|
||||
Set(string, any) error
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package keygen
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
// Bytes returns the byte from this ByteKey
|
||||
func (k ByteKey) Bytes() []byte {
|
||||
return []byte(k)
|
||||
}
|
||||
|
||||
func Random(n int) (ByteSource, error) {
|
||||
buf := make([]byte, n)
|
||||
if _, err := io.ReadFull(rand.Reader, buf); err != nil {
|
||||
return nil, fmt.Errorf(`failed to read from rand.Reader: %w`, err)
|
||||
}
|
||||
return ByteKey(buf), nil
|
||||
}
|
||||
|
||||
// Ecdhes generates a new key using ECDH-ES
|
||||
func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, apv []byte) (ByteSource, error) {
|
||||
priv, err := ecdsa.GenerateKey(pubkey.Curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to generate key for ECDH-ES: %w`, err)
|
||||
}
|
||||
|
||||
var algorithm string
|
||||
if alg == tokens.ECDH_ES {
|
||||
algorithm = enc
|
||||
} else {
|
||||
algorithm = alg
|
||||
}
|
||||
|
||||
pubinfo := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(pubinfo, uint32(keysize)*8)
|
||||
|
||||
if !priv.PublicKey.Curve.IsOnCurve(pubkey.X, pubkey.Y) {
|
||||
return nil, fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`)
|
||||
}
|
||||
z, _ := priv.PublicKey.Curve.ScalarMult(pubkey.X, pubkey.Y, priv.D.Bytes())
|
||||
zBytes := ecutil.AllocECPointBuffer(z, priv.PublicKey.Curve)
|
||||
defer ecutil.ReleaseECPointBuffer(zBytes)
|
||||
kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, apu, apv, pubinfo, []byte{})
|
||||
kek := make([]byte, keysize)
|
||||
if _, err := kdf.Read(kek); err != nil {
|
||||
return nil, fmt.Errorf(`failed to read kdf: %w`, err)
|
||||
}
|
||||
|
||||
return ByteWithECPublicKey{
|
||||
PublicKey: &priv.PublicKey,
|
||||
ByteKey: ByteKey(kek),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// X25519 generates a new key using ECDH-ES with X25519
|
||||
func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey) (ByteSource, error) {
|
||||
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to generate key for X25519: %w`, err)
|
||||
}
|
||||
|
||||
var algorithm string
|
||||
if alg == tokens.ECDH_ES {
|
||||
algorithm = enc
|
||||
} else {
|
||||
algorithm = alg
|
||||
}
|
||||
|
||||
pubinfo := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(pubinfo, uint32(keysize)*8)
|
||||
|
||||
zBytes, err := priv.ECDH(pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to compute Z: %w`, err)
|
||||
}
|
||||
kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, []byte{}, []byte{}, pubinfo, []byte{})
|
||||
kek := make([]byte, keysize)
|
||||
if _, err := kdf.Read(kek); err != nil {
|
||||
return nil, fmt.Errorf(`failed to read kdf: %w`, err)
|
||||
}
|
||||
|
||||
return ByteWithECPublicKey{
|
||||
PublicKey: priv.PublicKey(),
|
||||
ByteKey: ByteKey(kek),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HeaderPopulate populates the header with the required EC-DSA public key
|
||||
// information ('epk' key)
|
||||
func (k ByteWithECPublicKey) Populate(h Setter) error {
|
||||
key, err := jwk.Import(k.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to create JWK: %w`, err)
|
||||
}
|
||||
|
||||
if err := h.Set("epk", key); err != nil {
|
||||
return fmt.Errorf(`failed to write header: %w`, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HeaderPopulate populates the header with the required AES GCM
|
||||
// parameters ('iv' and 'tag')
|
||||
func (k ByteWithIVAndTag) Populate(h Setter) error {
|
||||
if err := h.Set("iv", k.IV); err != nil {
|
||||
return fmt.Errorf(`failed to write header: %w`, err)
|
||||
}
|
||||
|
||||
if err := h.Set("tag", k.Tag); err != nil {
|
||||
return fmt.Errorf(`failed to write header: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HeaderPopulate populates the header with the required PBES2
|
||||
// parameters ('p2s' and 'p2c')
|
||||
func (k ByteWithSaltAndCount) Populate(h Setter) error {
|
||||
if err := h.Set("p2c", k.Count); err != nil {
|
||||
return fmt.Errorf(`failed to write header: %w`, err)
|
||||
}
|
||||
|
||||
if err := h.Set("p2s", k.Salt); err != nil {
|
||||
return fmt.Errorf(`failed to write header: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user