feat: add aesgcm support
This commit is contained in:
parent
ec5c996e82
commit
ae81d0cdae
|
|
@ -72,7 +72,6 @@ func (c *AutoPushClient) Unregister(channelID string) (UnregisterResponse, error
|
|||
}
|
||||
|
||||
func (c *AutoPushClient) Decrypt(
|
||||
curve ecdh.Curve,
|
||||
authSecret []byte,
|
||||
useragentPrivateKey *ecdh.PrivateKey,
|
||||
notification Notification,
|
||||
|
|
@ -82,25 +81,16 @@ func (c *AutoPushClient) Decrypt(
|
|||
return nil, fmt.Errorf("base64 decode error: %v", err)
|
||||
}
|
||||
|
||||
payload, err := rfc8291.Unmarshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rfc8291 decode error: %v", err)
|
||||
}
|
||||
|
||||
appserverPublicKey, err := curve.NewPublicKey(payload.KeyId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ecdh public key load error: %v", err)
|
||||
}
|
||||
|
||||
plaintext, err := c.ece.Decrypt(
|
||||
payload.CipherText,
|
||||
payload.Salt,
|
||||
data,
|
||||
rfc8291.Encoding(notification.Headers.Encoding),
|
||||
notification.Headers.Encryption,
|
||||
notification.Headers.CryptoKey,
|
||||
authSecret,
|
||||
useragentPrivateKey,
|
||||
appserverPublicKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rfc8291 decrypt error: %v", err)
|
||||
return nil, fmt.Errorf("decrypt error: %v", err)
|
||||
}
|
||||
|
||||
var result webpush.WebPushPayload
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func appserver(authSecret []byte, useragentPublicKey *ecdh.PublicKey) []byte {
|
|||
_, salt, privateKey := rfc8291.NewSecrets(CURVE)
|
||||
ece := rfc8291.NewRFC8291(HASH)
|
||||
|
||||
encrypted, err := ece.Encrypt(
|
||||
encrypted, err := ece.EncryptAes128gcm(
|
||||
[]byte(PLAINTEXT),
|
||||
salt,
|
||||
authSecret,
|
||||
|
|
@ -53,7 +53,7 @@ func main() {
|
|||
log.Panicln("Load PublicKey Error", err)
|
||||
}
|
||||
|
||||
plaintext, err := ece.Decrypt(
|
||||
plaintext, err := ece.DecryptAes128gcm(
|
||||
payload.CipherText,
|
||||
payload.Salt,
|
||||
authSecret,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,154 @@
|
|||
package rfc8291
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// Aes128gcmScheme implements EncodingScheme for the aes128gcm encoding.
|
||||
type Aes128gcmScheme struct{}
|
||||
|
||||
func (s Aes128gcmScheme) DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error) {
|
||||
prkKey := hkdf.Extract(hash, ecdhSecret, authSecret)
|
||||
|
||||
// aes128gcm: "WebPush: info\0" + receiver key + sender key
|
||||
keyInfo := bytes.Join([][]byte{
|
||||
[]byte("WebPush: info\000"),
|
||||
uaKey.Bytes(),
|
||||
asKey.Bytes(),
|
||||
}, nil)
|
||||
|
||||
ikm := make([]byte, HKDF_IKM_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prkKey, keyInfo), ikm); err != nil {
|
||||
return nil, fmt.Errorf("derive IKM failed: %v", err)
|
||||
}
|
||||
|
||||
return ikm, nil
|
||||
}
|
||||
|
||||
func (s Aes128gcmScheme) DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error) {
|
||||
prk := hkdf.Extract(hash, ikm, salt)
|
||||
|
||||
// aes128gcm: simple info strings without keys
|
||||
cekInfo := []byte("Content-Encoding: aes128gcm\000")
|
||||
nonceInfo := []byte("Content-Encoding: nonce\000")
|
||||
|
||||
cek = make([]byte, HKDF_CEK_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prk, cekInfo), cek); err != nil {
|
||||
return nil, nil, fmt.Errorf("derive CEK failed: %v", err)
|
||||
}
|
||||
|
||||
nonce = make([]byte, HKDF_NONCE_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prk, nonceInfo), nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("derive nonce failed: %v", err)
|
||||
}
|
||||
|
||||
return cek, nonce, nil
|
||||
}
|
||||
|
||||
func (s Aes128gcmScheme) Pad(plaintext []byte) []byte {
|
||||
// aes128gcm: append 0x02 delimiter for final record
|
||||
return append(plaintext, 0x02)
|
||||
}
|
||||
|
||||
func (s Aes128gcmScheme) Unpad(data []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, fmt.Errorf("data is empty")
|
||||
}
|
||||
// aes128gcm: remove trailing 0x01 or 0x02 delimiter
|
||||
last := data[len(data)-1]
|
||||
if last == 0x01 || last == 0x02 {
|
||||
return data[:len(data)-1], nil
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// Payload represents the aes128gcm message format with embedded crypto parameters.
|
||||
type Payload struct {
|
||||
RS uint32
|
||||
Salt []byte
|
||||
KeyId []byte
|
||||
CipherText []byte
|
||||
}
|
||||
|
||||
const (
|
||||
baseHeaderLen = 21
|
||||
)
|
||||
|
||||
// Marshal serializes a Payload into the aes128gcm binary format.
|
||||
func Marshal(p Payload) (data []byte) {
|
||||
rs := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(rs, p.RS)
|
||||
return bytes.Join([][]byte{
|
||||
p.Salt,
|
||||
rs,
|
||||
{uint8(len(p.KeyId))},
|
||||
p.KeyId,
|
||||
p.CipherText,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
// Unmarshal parses the aes128gcm binary format into a Payload.
|
||||
func Unmarshal(data []byte) (p Payload, err error) {
|
||||
if len(data) < baseHeaderLen {
|
||||
return p, errors.New("data is too short")
|
||||
}
|
||||
|
||||
p.Salt = data[:16]
|
||||
p.RS = binary.BigEndian.Uint32(data[16:20])
|
||||
|
||||
idlen := int(data[20])
|
||||
if len(data) < baseHeaderLen+idlen {
|
||||
return p, errors.New("data is too short")
|
||||
}
|
||||
|
||||
if idlen > 0 {
|
||||
p.KeyId = data[baseHeaderLen : baseHeaderLen+idlen]
|
||||
}
|
||||
|
||||
p.CipherText = data[baseHeaderLen+idlen:]
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// EncryptAes128gcm encrypts a message using the aes128gcm encoding scheme.
|
||||
// Returns the complete payload with embedded crypto parameters.
|
||||
func (c *RFC8291) EncryptAes128gcm(
|
||||
plaintext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
receiverPublicKey *ecdh.PublicKey,
|
||||
senderPrivateKey *ecdh.PrivateKey,
|
||||
) ([]byte, error) {
|
||||
ciphertext, err := c.encrypt(Aes128gcmScheme{}, plaintext, salt, authSecret, receiverPublicKey, senderPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs := uint32(len(plaintext) + 1 + AES_GCM_OVERHEAD)
|
||||
|
||||
return Marshal(Payload{
|
||||
RS: rs,
|
||||
Salt: salt,
|
||||
KeyId: senderPrivateKey.PublicKey().Bytes(),
|
||||
CipherText: ciphertext,
|
||||
}), nil
|
||||
}
|
||||
|
||||
// DecryptAes128gcm decrypts a message encrypted with the aes128gcm encoding scheme.
|
||||
func (c *RFC8291) DecryptAes128gcm(
|
||||
ciphertext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
receiverPrivateKey *ecdh.PrivateKey,
|
||||
senderPublicKey *ecdh.PublicKey,
|
||||
) ([]byte, error) {
|
||||
return c.decrypt(Aes128gcmScheme{}, ciphertext, salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
package rfc8291
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdh"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// AesgcmScheme implements EncodingScheme for the aesgcm encoding.
|
||||
type AesgcmScheme struct{}
|
||||
|
||||
func (s AesgcmScheme) DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error) {
|
||||
prkKey := hkdf.Extract(hash, ecdhSecret, authSecret)
|
||||
|
||||
// aesgcm: just "Content-Encoding: auth\0" without keys
|
||||
keyInfo := []byte("Content-Encoding: auth\000")
|
||||
|
||||
ikm := make([]byte, HKDF_IKM_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prkKey, keyInfo), ikm); err != nil {
|
||||
return nil, fmt.Errorf("derive IKM failed: %v", err)
|
||||
}
|
||||
|
||||
return ikm, nil
|
||||
}
|
||||
|
||||
func (s AesgcmScheme) DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error) {
|
||||
prk := hkdf.Extract(hash, ikm, salt)
|
||||
|
||||
// aesgcm: info includes "P-256\0" and length-prefixed public keys
|
||||
uaKeyBytes := uaKey.Bytes()
|
||||
asKeyBytes := asKey.Bytes()
|
||||
|
||||
context := bytes.Join([][]byte{
|
||||
[]byte("P-256\000"),
|
||||
{0, byte(len(uaKeyBytes))},
|
||||
uaKeyBytes,
|
||||
{0, byte(len(asKeyBytes))},
|
||||
asKeyBytes,
|
||||
}, nil)
|
||||
|
||||
cekInfo := append([]byte("Content-Encoding: aesgcm\000"), context...)
|
||||
nonceInfo := append([]byte("Content-Encoding: nonce\000"), context...)
|
||||
|
||||
cek = make([]byte, HKDF_CEK_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prk, cekInfo), cek); err != nil {
|
||||
return nil, nil, fmt.Errorf("derive CEK failed: %v", err)
|
||||
}
|
||||
|
||||
nonce = make([]byte, HKDF_NONCE_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(hash, prk, nonceInfo), nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("derive nonce failed: %v", err)
|
||||
}
|
||||
|
||||
return cek, nonce, nil
|
||||
}
|
||||
|
||||
func (s AesgcmScheme) Pad(plaintext []byte) []byte {
|
||||
// aesgcm: 2-byte big-endian padding length prefix (0 padding)
|
||||
result := make([]byte, 2+len(plaintext))
|
||||
// First two bytes are 0 (no padding), already zero-initialized
|
||||
copy(result[2:], plaintext)
|
||||
return result
|
||||
}
|
||||
|
||||
func (s AesgcmScheme) Unpad(data []byte) ([]byte, error) {
|
||||
if len(data) < 2 {
|
||||
return nil, fmt.Errorf("data too short for aesgcm padding")
|
||||
}
|
||||
|
||||
padLen := int(data[0])<<8 | int(data[1])
|
||||
|
||||
if 2+padLen > len(data) {
|
||||
return nil, fmt.Errorf("invalid padding length: %d (data length: %d)", padLen, len(data))
|
||||
}
|
||||
|
||||
// Verify padding bytes are all zeros
|
||||
for i := 2; i < 2+padLen; i++ {
|
||||
if data[i] != 0 {
|
||||
return nil, fmt.Errorf("invalid padding: non-zero byte at position %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
return data[2+padLen:], nil
|
||||
}
|
||||
|
||||
// EncryptResult holds the result of aesgcm encryption.
|
||||
// Unlike aes128gcm which embeds crypto params in the payload,
|
||||
// aesgcm requires these to be sent as HTTP headers.
|
||||
type EncryptResult struct {
|
||||
Ciphertext []byte // The encrypted data (for request body)
|
||||
Salt []byte // For Encryption header: salt=<base64url>
|
||||
SenderPublicKey []byte // For Crypto-Key header: dh=<base64url>
|
||||
}
|
||||
|
||||
// CryptoParams holds the extracted cryptographic parameters for decryption.
|
||||
type CryptoParams struct {
|
||||
Salt []byte
|
||||
SenderPublicKey *ecdh.PublicKey
|
||||
}
|
||||
|
||||
// ParseAesgcmHeaders extracts salt and sender public key from aesgcm HTTP headers.
|
||||
// encryptionHeader: e.g., "salt=FiyMDLvlVl678odI9AWL3A"
|
||||
// cryptoKeyHeader: e.g., "dh=BMLYo...;p256ecdsa=BF5o..."
|
||||
func ParseAesgcmHeaders(encryptionHeader, cryptoKeyHeader string, curve ecdh.Curve) (*CryptoParams, error) {
|
||||
salt, err := parseHeaderParam(encryptionHeader, "salt")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse salt: %v", err)
|
||||
}
|
||||
if len(salt) != SALT_LEN {
|
||||
return nil, fmt.Errorf("salt must be %d bytes, got %d", SALT_LEN, len(salt))
|
||||
}
|
||||
|
||||
dhBytes, err := parseHeaderParam(cryptoKeyHeader, "dh")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse dh: %v", err)
|
||||
}
|
||||
|
||||
senderPublicKey, err := curve.NewPublicKey(dhBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sender public key: %v", err)
|
||||
}
|
||||
|
||||
return &CryptoParams{
|
||||
Salt: salt,
|
||||
SenderPublicKey: senderPublicKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// parseHeaderParam extracts a base64url-encoded parameter value from a header string.
|
||||
func parseHeaderParam(header, paramName string) ([]byte, error) {
|
||||
parts := strings.FieldsFunc(header, func(r rune) bool {
|
||||
return r == ';' || r == ','
|
||||
})
|
||||
|
||||
prefix := paramName + "="
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if strings.HasPrefix(part, prefix) {
|
||||
value := strings.TrimPrefix(part, prefix)
|
||||
value = strings.Trim(value, "\"")
|
||||
return base64.RawURLEncoding.DecodeString(value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("parameter %q not found in header", paramName)
|
||||
}
|
||||
|
||||
// EncryptAesgcm encrypts a message using the aesgcm encoding scheme.
|
||||
// Returns the ciphertext and crypto parameters needed for HTTP headers.
|
||||
func (c *RFC8291) EncryptAesgcm(
|
||||
plaintext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
receiverPublicKey *ecdh.PublicKey,
|
||||
senderPrivateKey *ecdh.PrivateKey,
|
||||
) (*EncryptResult, error) {
|
||||
ciphertext, err := c.encrypt(AesgcmScheme{}, plaintext, salt, authSecret, receiverPublicKey, senderPrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EncryptResult{
|
||||
Ciphertext: ciphertext,
|
||||
Salt: salt,
|
||||
SenderPublicKey: senderPrivateKey.PublicKey().Bytes(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecryptAesgcm decrypts a message encrypted with the aesgcm encoding scheme.
|
||||
func (c *RFC8291) DecryptAesgcm(
|
||||
ciphertext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
receiverPrivateKey *ecdh.PrivateKey,
|
||||
senderPublicKey *ecdh.PublicKey,
|
||||
) ([]byte, error) {
|
||||
return c.decrypt(AesgcmScheme{}, ciphertext, salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package rfc8291
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const (
|
||||
BASE_HEADER_LEN = 21
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
RS uint32
|
||||
Salt []byte
|
||||
KeyId []byte
|
||||
CipherText []byte
|
||||
}
|
||||
|
||||
func Marshal(p Payload) (data []byte) {
|
||||
rs := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(rs, p.RS)
|
||||
return bytes.Join([][]byte{
|
||||
p.Salt,
|
||||
rs,
|
||||
{uint8(len(p.KeyId))},
|
||||
p.KeyId,
|
||||
p.CipherText,
|
||||
}, []byte{})
|
||||
}
|
||||
|
||||
func Unmarshal(data []byte) (p Payload, err error) {
|
||||
err = errors.New("data is too short")
|
||||
|
||||
if len(data) < BASE_HEADER_LEN {
|
||||
return p, err
|
||||
}
|
||||
|
||||
p.Salt = data[:16]
|
||||
p.RS = binary.BigEndian.Uint32(data[16:20])
|
||||
|
||||
idlen := int(data[20])
|
||||
if len(data) < BASE_HEADER_LEN+idlen {
|
||||
return p, err
|
||||
}
|
||||
|
||||
if idlen > 0 {
|
||||
p.KeyId = data[BASE_HEADER_LEN : BASE_HEADER_LEN+idlen]
|
||||
}
|
||||
|
||||
p.CipherText = data[BASE_HEADER_LEN+idlen:]
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package rfc8291
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/ecdh"
|
||||
|
|
@ -11,8 +10,6 @@ import (
|
|||
"hash"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -26,11 +23,40 @@ const (
|
|||
HKDF_NONCE_LEN = 12
|
||||
)
|
||||
|
||||
// Encoding represents the Content-Encoding type for WebPush messages.
|
||||
type Encoding string
|
||||
|
||||
const (
|
||||
EncodingAes128gcm Encoding = "aes128gcm"
|
||||
EncodingAesgcm Encoding = "aesgcm"
|
||||
)
|
||||
|
||||
// EncodingScheme defines the encoding-specific operations for WebPush encryption.
|
||||
type EncodingScheme interface {
|
||||
DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error)
|
||||
DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error)
|
||||
Pad(plaintext []byte) []byte
|
||||
Unpad(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// Scheme returns the EncodingScheme implementation for the given encoding type.
|
||||
func Scheme(encoding Encoding) (EncodingScheme, error) {
|
||||
switch encoding {
|
||||
case EncodingAes128gcm:
|
||||
return Aes128gcmScheme{}, nil
|
||||
case EncodingAesgcm:
|
||||
return AesgcmScheme{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
// RFC8291 implements WebPush message encryption and decryption.
|
||||
type RFC8291 struct {
|
||||
hash func() hash.Hash
|
||||
}
|
||||
|
||||
// Default Hash is SHA256
|
||||
// NewRFC8291 creates a new RFC8291 instance. Default hash is SHA256.
|
||||
func NewRFC8291(hash func() hash.Hash) *RFC8291 {
|
||||
if hash == nil {
|
||||
hash = sha256.New
|
||||
|
|
@ -38,6 +64,7 @@ func NewRFC8291(hash func() hash.Hash) *RFC8291 {
|
|||
return &RFC8291{hash: hash}
|
||||
}
|
||||
|
||||
// NewSecrets generates new random auth secret, salt, and ECDH private key.
|
||||
func NewSecrets(curve ecdh.Curve) (auth, salt []byte, key *ecdh.PrivateKey) {
|
||||
auth = make([]byte, AUTH_SECRET_LEN)
|
||||
salt = make([]byte, SALT_LEN)
|
||||
|
|
@ -56,12 +83,14 @@ func NewSecrets(curve ecdh.Curve) (auth, salt []byte, key *ecdh.PrivateKey) {
|
|||
return auth, salt, key
|
||||
}
|
||||
|
||||
func (c *RFC8291) Encrypt(
|
||||
// encrypt performs encryption using the specified encoding scheme.
|
||||
func (c *RFC8291) encrypt(
|
||||
scheme EncodingScheme,
|
||||
plaintext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
useragentPublicKey *ecdh.PublicKey,
|
||||
appserverPrivateKey *ecdh.PrivateKey,
|
||||
receiverPublicKey *ecdh.PublicKey,
|
||||
senderPrivateKey *ecdh.PrivateKey,
|
||||
) ([]byte, error) {
|
||||
if len(authSecret) != AUTH_SECRET_LEN {
|
||||
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
||||
|
|
@ -70,17 +99,17 @@ func (c *RFC8291) Encrypt(
|
|||
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
||||
}
|
||||
|
||||
ecdhSecret, err := appserverPrivateKey.ECDH(useragentPublicKey)
|
||||
ecdhSecret, err := senderPrivateKey.ECDH(receiverPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
||||
}
|
||||
|
||||
ikm, err := c.ikm(authSecret, ecdhSecret, useragentPublicKey, appserverPrivateKey.PublicKey())
|
||||
ikm, err := scheme.DeriveIKM(c.hash, authSecret, ecdhSecret, receiverPublicKey, senderPrivateKey.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cek, nonce, err := c.cekAndNonce(ikm, salt)
|
||||
cek, nonce, err := scheme.DeriveCEKAndNonce(c.hash, ikm, salt, receiverPublicKey, senderPrivateKey.PublicKey())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -90,27 +119,20 @@ func (c *RFC8291) Encrypt(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
||||
rs := uint32(len(plaintext) + 1 + AES_GCM_OVERHEAD)
|
||||
paddedPlaintext := scheme.Pad(plaintext)
|
||||
ciphertext := gcm.Seal(nil, nonce, paddedPlaintext, nil)
|
||||
|
||||
// RFC8188: 0x01 or 0x02 in tail of plaintext data
|
||||
// RFC8291: The push message plaintext has the padding delimiter octet (0x02) appended to produce
|
||||
// ciphertext = bytes.Join([][]byte{ciphertext, {0x02}}, []byte{})
|
||||
|
||||
return Marshal(Payload{
|
||||
RS: rs,
|
||||
Salt: salt,
|
||||
KeyId: appserverPrivateKey.PublicKey().Bytes(),
|
||||
CipherText: ciphertext,
|
||||
}), nil
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func (c *RFC8291) Decrypt(
|
||||
// decrypt performs decryption using the specified encoding scheme.
|
||||
func (c *RFC8291) decrypt(
|
||||
scheme EncodingScheme,
|
||||
ciphertext []byte,
|
||||
salt []byte,
|
||||
authSecret []byte,
|
||||
useragentPrivateKey *ecdh.PrivateKey,
|
||||
appserverPublicKey *ecdh.PublicKey,
|
||||
receiverPrivateKey *ecdh.PrivateKey,
|
||||
senderPublicKey *ecdh.PublicKey,
|
||||
) ([]byte, error) {
|
||||
if len(authSecret) != AUTH_SECRET_LEN {
|
||||
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
||||
|
|
@ -119,17 +141,17 @@ func (c *RFC8291) Decrypt(
|
|||
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
||||
}
|
||||
|
||||
ecdhSecret, err := useragentPrivateKey.ECDH(appserverPublicKey)
|
||||
ecdhSecret, err := receiverPrivateKey.ECDH(senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
||||
}
|
||||
|
||||
ikm, err := c.ikm(authSecret, ecdhSecret, useragentPrivateKey.PublicKey(), appserverPublicKey)
|
||||
ikm, err := scheme.DeriveIKM(c.hash, authSecret, ecdhSecret, receiverPrivateKey.PublicKey(), senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cek, nonce, err := c.cekAndNonce(ikm, salt)
|
||||
cek, nonce, err := scheme.DeriveCEKAndNonce(c.hash, ikm, salt, receiverPrivateKey.PublicKey(), senderPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -144,13 +166,7 @@ func (c *RFC8291) Decrypt(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// RFC8188: 0x01 or 0x02 in tail of plaintext data
|
||||
// RFC8291: The push message plaintext has the padding delimiter octet (0x02) appended to produce
|
||||
if plaintext[len(plaintext)-1] == 0x01 || plaintext[len(plaintext)-1] == 0x02 {
|
||||
plaintext = plaintext[:len(plaintext)-1]
|
||||
}
|
||||
|
||||
return plaintext, err
|
||||
return scheme.Unpad(plaintext)
|
||||
}
|
||||
|
||||
func (c *RFC8291) gcm(cek []byte) (cipher.AEAD, error) {
|
||||
|
|
@ -167,43 +183,42 @@ func (c *RFC8291) gcm(cek []byte) (cipher.AEAD, error) {
|
|||
return gcm, nil
|
||||
}
|
||||
|
||||
func (c *RFC8291) ikm(
|
||||
// Decrypt decrypts a push notification, automatically selecting the correct
|
||||
// encoding scheme based on the encoding parameter.
|
||||
//
|
||||
// For aes128gcm: crypto params are extracted from the data payload.
|
||||
// For aesgcm: crypto params are extracted from the HTTP headers.
|
||||
func (c *RFC8291) Decrypt(
|
||||
data []byte,
|
||||
encoding Encoding,
|
||||
encryptionHeader string,
|
||||
cryptoKeyHeader string,
|
||||
authSecret []byte,
|
||||
ecdhSecret []byte,
|
||||
useragentPublicKey *ecdh.PublicKey,
|
||||
appserverPublicKey *ecdh.PublicKey,
|
||||
) (ikm []byte, err error) {
|
||||
prkKey := hkdf.Extract(c.hash, ecdhSecret, authSecret)
|
||||
|
||||
keyInfo := bytes.Join([][]byte{
|
||||
[]byte("WebPush: info\000"),
|
||||
useragentPublicKey.Bytes(),
|
||||
appserverPublicKey.Bytes(),
|
||||
}, []byte{})
|
||||
|
||||
ikm = make([]byte, HKDF_IKM_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prkKey, keyInfo), ikm); err != nil {
|
||||
return nil, fmt.Errorf("read IKM failed: %v", err)
|
||||
receiverPrivateKey *ecdh.PrivateKey,
|
||||
) ([]byte, error) {
|
||||
switch encoding {
|
||||
case EncodingAes128gcm:
|
||||
payload, err := Unmarshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal aes128gcm payload: %v", err)
|
||||
}
|
||||
|
||||
return ikm, nil
|
||||
}
|
||||
|
||||
func (c *RFC8291) cekAndNonce(ikm []byte, salt []byte) (cek, nonce []byte, err error) {
|
||||
prk := hkdf.Extract(c.hash, ikm, salt)
|
||||
|
||||
cekInfo := []byte("Content-Encoding: aes128gcm\000")
|
||||
nonceInfo := []byte("Content-Encoding: nonce\000")
|
||||
|
||||
cek = make([]byte, HKDF_CEK_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prk, cekInfo), cek); err != nil {
|
||||
return nil, nil, fmt.Errorf("read CEK failed: %v", err)
|
||||
}
|
||||
|
||||
nonce = make([]byte, HKDF_NONCE_LEN)
|
||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prk, nonceInfo), nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("read Nonce failed: %v", err)
|
||||
}
|
||||
|
||||
return cek, nonce, nil
|
||||
senderPublicKey, err := receiverPrivateKey.Curve().NewPublicKey(payload.KeyId)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse sender public key: %v", err)
|
||||
}
|
||||
|
||||
return c.DecryptAes128gcm(payload.CipherText, payload.Salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||
|
||||
case EncodingAesgcm:
|
||||
params, err := ParseAesgcmHeaders(encryptionHeader, cryptoKeyHeader, receiverPrivateKey.Curve())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse aesgcm headers: %v", err)
|
||||
}
|
||||
|
||||
return c.DecryptAesgcm(data, params.Salt, authSecret, receiverPrivateKey, params.SenderPublicKey)
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,6 @@ func (c *NicoPushClient) Register() (channelID string, err error) {
|
|||
|
||||
func (c *NicoPushClient) Decrypt(data autopush.Notification) (*webpush.WebPushPayload, error) {
|
||||
return c.autoPushClient.Decrypt(
|
||||
ecdh.P256(),
|
||||
c.authSecret,
|
||||
c.privateKey,
|
||||
data,
|
||||
|
|
|
|||
Loading…
Reference in New Issue