mirror of
https://source.quilibrium.com/quilibrium/ceremonyclient.git
synced 2024-12-27 00:55:17 +00:00
452 lines
12 KiB
Go
452 lines
12 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha512"
|
|
"crypto/subtle"
|
|
"encoding/binary"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/hkdf"
|
|
"source.quilibrium.com/quilibrium/monorepo/nekryptology/pkg/core/curves"
|
|
"source.quilibrium.com/quilibrium/monorepo/node/keys"
|
|
"source.quilibrium.com/quilibrium/monorepo/node/protobufs"
|
|
)
|
|
|
|
const DOUBLE_RATCHET_PROTOCOL_VERSION = 1
|
|
const DOUBLE_RATCHET_PROTOCOL = 1<<8 + DOUBLE_RATCHET_PROTOCOL_VERSION
|
|
|
|
const CHAIN_KEY = 0x01
|
|
const MESSAGE_KEY = 0x02
|
|
const AEAD_KEY = 0x03
|
|
|
|
// Note: If an HSM with raw primitive access becomes available, the raw crypto
|
|
// mechanisms should be refactored into calls in KeyManager and implemented
|
|
// through the driver
|
|
type DoubleRatchetParticipant struct {
|
|
sendingEphemeralPrivateKey curves.Scalar
|
|
receivingEphemeralKey curves.Point
|
|
curve *curves.Curve
|
|
keyManager keys.KeyManager
|
|
rootKey []byte
|
|
sendingChainKey []byte
|
|
currentSendingHeaderKey []byte
|
|
currentReceivingHeaderKey []byte
|
|
nextSendingHeaderKey []byte
|
|
nextReceivingHeaderKey []byte
|
|
receivingChainKey []byte
|
|
currentSendingChainLength uint32
|
|
previousSendingChainLength uint32
|
|
currentReceivingChainLength uint32
|
|
previousReceivingChainLength uint32
|
|
skippedKeysMap map[string]map[uint32][]byte
|
|
}
|
|
|
|
func NewDoubleRatchetParticipant(
|
|
sessionKey []byte,
|
|
sendingHeaderKey []byte,
|
|
nextReceivingHeaderKey []byte,
|
|
isSender bool,
|
|
sendingEphemeralPrivateKey curves.Scalar,
|
|
receivingEphemeralKey curves.Point,
|
|
curve *curves.Curve,
|
|
keyManager keys.KeyManager,
|
|
) (*DoubleRatchetParticipant, error) {
|
|
participant := &DoubleRatchetParticipant{}
|
|
participant.sendingEphemeralPrivateKey = sendingEphemeralPrivateKey
|
|
participant.skippedKeysMap = make(map[string]map[uint32][]byte)
|
|
participant.keyManager = keyManager
|
|
participant.currentSendingChainLength = 0
|
|
participant.previousSendingChainLength = 0
|
|
participant.currentReceivingChainLength = 0
|
|
participant.previousReceivingChainLength = 0
|
|
|
|
if sendingEphemeralPrivateKey.Point().CurveName() !=
|
|
receivingEphemeralKey.CurveName() || receivingEphemeralKey.CurveName() !=
|
|
curve.Name {
|
|
return nil, errors.New("curve mismatch")
|
|
}
|
|
|
|
participant.curve = curve
|
|
|
|
if isSender {
|
|
hash := hkdf.New(
|
|
sha512.New,
|
|
receivingEphemeralKey.Mul(
|
|
sendingEphemeralPrivateKey,
|
|
).ToAffineCompressed(),
|
|
sessionKey,
|
|
[]byte("quilibrium-double-ratchet"),
|
|
)
|
|
rkck := make([]byte, 96)
|
|
if _, err := hash.Read(rkck[:]); err != nil {
|
|
return nil, errors.Wrap(err, "failed establishing root key")
|
|
}
|
|
|
|
participant.currentSendingHeaderKey = sendingHeaderKey
|
|
participant.nextReceivingHeaderKey = nextReceivingHeaderKey
|
|
participant.rootKey = rkck[:32]
|
|
participant.sendingChainKey = rkck[32:64]
|
|
|
|
participant.nextSendingHeaderKey = rkck[64:96]
|
|
participant.receivingEphemeralKey = receivingEphemeralKey
|
|
} else {
|
|
participant.rootKey = sessionKey
|
|
participant.nextReceivingHeaderKey = sendingHeaderKey
|
|
participant.nextSendingHeaderKey = nextReceivingHeaderKey
|
|
}
|
|
|
|
return participant, nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) RatchetEncrypt(
|
|
message []byte,
|
|
) (*protobufs.P2PChannelEnvelope, error) {
|
|
envelope := &protobufs.P2PChannelEnvelope{
|
|
ProtocolIdentifier: DOUBLE_RATCHET_PROTOCOL,
|
|
MessageHeader: &protobufs.MessageCiphertext{},
|
|
MessageBody: &protobufs.MessageCiphertext{},
|
|
}
|
|
|
|
newChainKey, messageKey, aeadKey := ratchetKeys(r.sendingChainKey)
|
|
r.sendingChainKey = newChainKey
|
|
|
|
var err error
|
|
header := r.encodeHeader()
|
|
envelope.MessageHeader, err = r.encrypt(
|
|
header,
|
|
r.currentSendingHeaderKey,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not encrypt header")
|
|
}
|
|
|
|
envelope.MessageBody, err = r.encrypt(
|
|
message,
|
|
messageKey,
|
|
append(append([]byte{}, aeadKey...), envelope.MessageHeader.Ciphertext...),
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not encrypt message")
|
|
}
|
|
|
|
r.currentSendingChainLength++
|
|
|
|
return envelope, nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) RatchetDecrypt(
|
|
envelope *protobufs.P2PChannelEnvelope,
|
|
) ([]byte, error) {
|
|
plaintext, err := r.trySkippedMessageKeys(envelope)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decrypt from matching skipped key")
|
|
}
|
|
|
|
if plaintext != nil {
|
|
return plaintext, nil
|
|
}
|
|
|
|
header, shouldRatchet, err := r.decryptHeader(
|
|
envelope.MessageHeader,
|
|
r.currentReceivingHeaderKey,
|
|
)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decrypt header")
|
|
}
|
|
|
|
receivingEphemeralKey,
|
|
previousReceivingChainLength,
|
|
currentReceivingChainLength,
|
|
err := r.decodeHeader(header)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decode header")
|
|
}
|
|
|
|
if shouldRatchet {
|
|
if err := r.skipMessageKeys(previousReceivingChainLength); err != nil {
|
|
return nil, errors.Wrap(err, "could not skip previous message keys")
|
|
}
|
|
if err := r.ratchetEphemeralKeys(receivingEphemeralKey); err != nil {
|
|
return nil, errors.Wrap(err, "could not ratchet ephemeral keys")
|
|
}
|
|
}
|
|
|
|
if err := r.skipMessageKeys(currentReceivingChainLength); err != nil {
|
|
return nil, errors.Wrap(err, "could not skip message keys")
|
|
}
|
|
|
|
newChainKey, messageKey, aeadKey := ratchetKeys(r.receivingChainKey)
|
|
|
|
plaintext, err = r.decrypt(
|
|
envelope.MessageBody,
|
|
messageKey,
|
|
append(
|
|
append([]byte{}, aeadKey...),
|
|
envelope.MessageHeader.Ciphertext...,
|
|
),
|
|
)
|
|
|
|
r.receivingChainKey = newChainKey
|
|
r.currentReceivingChainLength++
|
|
|
|
return plaintext, errors.Wrap(err, "could not decrypt message")
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) ratchetEphemeralKeys(
|
|
newReceivingEphemeralKey curves.Point,
|
|
) error {
|
|
r.previousSendingChainLength = r.currentSendingChainLength
|
|
r.currentSendingChainLength = 0
|
|
r.currentReceivingChainLength = 0
|
|
r.currentSendingHeaderKey = r.nextSendingHeaderKey
|
|
r.currentReceivingHeaderKey = r.nextReceivingHeaderKey
|
|
r.receivingEphemeralKey = newReceivingEphemeralKey
|
|
|
|
hash := hkdf.New(
|
|
sha512.New,
|
|
newReceivingEphemeralKey.Mul(
|
|
r.sendingEphemeralPrivateKey,
|
|
).ToAffineCompressed(),
|
|
r.rootKey,
|
|
[]byte("quilibrium-double-ratchet"),
|
|
)
|
|
rkck := make([]byte, 96)
|
|
if _, err := hash.Read(rkck[:]); err != nil {
|
|
return errors.Wrap(err, "failed ratcheting root key")
|
|
}
|
|
|
|
r.rootKey = rkck[:32]
|
|
r.receivingChainKey = rkck[32:64]
|
|
r.nextReceivingHeaderKey = rkck[64:]
|
|
r.sendingEphemeralPrivateKey = r.curve.NewScalar().Random(rand.Reader)
|
|
|
|
hash = hkdf.New(
|
|
sha512.New,
|
|
newReceivingEphemeralKey.Mul(
|
|
r.sendingEphemeralPrivateKey,
|
|
).ToAffineCompressed(),
|
|
r.rootKey,
|
|
[]byte("quilibrium-double-ratchet"),
|
|
)
|
|
rkck2 := make([]byte, 96)
|
|
if _, err := hash.Read(rkck2[:]); err != nil {
|
|
return errors.Wrap(err, "failed ratcheting root key")
|
|
}
|
|
|
|
r.rootKey = rkck2[:32]
|
|
r.sendingChainKey = rkck2[32:64]
|
|
r.nextSendingHeaderKey = rkck2[64:]
|
|
return nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) trySkippedMessageKeys(
|
|
envelope *protobufs.P2PChannelEnvelope,
|
|
) ([]byte, error) {
|
|
for receivingHeaderKey, skippedKeys := range r.skippedKeysMap {
|
|
header, _, err := r.decryptHeader(
|
|
envelope.MessageHeader,
|
|
[]byte(receivingHeaderKey),
|
|
)
|
|
|
|
if err == nil {
|
|
_, _, current, err := r.decodeHeader(header)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "malformed header")
|
|
}
|
|
|
|
messageKey := skippedKeys[current][:32]
|
|
aeadKey := skippedKeys[current][32:]
|
|
plaintext, err := r.decrypt(
|
|
envelope.MessageBody,
|
|
messageKey,
|
|
append(
|
|
append([]byte{}, aeadKey...),
|
|
envelope.MessageHeader.Ciphertext[:]...,
|
|
),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not decrypt from skipped key")
|
|
}
|
|
|
|
delete(r.skippedKeysMap[receivingHeaderKey], current)
|
|
if len(r.skippedKeysMap[receivingHeaderKey]) == 0 {
|
|
delete(r.skippedKeysMap, receivingHeaderKey)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) skipMessageKeys(until uint32) error {
|
|
if r.currentReceivingChainLength+100 < until {
|
|
return errors.New("skip limit exceeded")
|
|
}
|
|
|
|
if r.receivingChainKey != nil {
|
|
for r.currentReceivingChainLength < until {
|
|
newChainKey, messageKey, aeadKey := ratchetKeys(r.receivingChainKey)
|
|
skippedKeys := r.skippedKeysMap[string(r.currentReceivingHeaderKey)]
|
|
if skippedKeys == nil {
|
|
r.skippedKeysMap[string(r.currentReceivingHeaderKey)] =
|
|
make(map[uint32][]byte)
|
|
}
|
|
|
|
skippedKeys[r.currentReceivingChainLength] = append(
|
|
append([]byte{}, messageKey...),
|
|
aeadKey...,
|
|
)
|
|
r.receivingChainKey = newChainKey
|
|
r.currentReceivingChainLength++
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) encodeHeader() []byte {
|
|
header := []byte{}
|
|
header = append(
|
|
header,
|
|
r.curve.NewGeneratorPoint().Mul(
|
|
r.sendingEphemeralPrivateKey,
|
|
).ToAffineCompressed()[:]...,
|
|
)
|
|
header = binary.BigEndian.AppendUint32(header, r.previousSendingChainLength)
|
|
header = binary.BigEndian.AppendUint32(header, r.currentSendingChainLength)
|
|
return header
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) decryptHeader(
|
|
ciphertext *protobufs.MessageCiphertext,
|
|
receivingHeaderKey []byte,
|
|
) ([]byte, bool, error) {
|
|
header, err := r.decrypt(
|
|
ciphertext,
|
|
receivingHeaderKey,
|
|
nil,
|
|
)
|
|
if err != nil && subtle.ConstantTimeCompare(
|
|
r.currentReceivingHeaderKey,
|
|
receivingHeaderKey,
|
|
) == 1 {
|
|
if header, err = r.decrypt(
|
|
ciphertext,
|
|
r.nextReceivingHeaderKey,
|
|
nil,
|
|
); err != nil {
|
|
return nil, false, errors.Wrap(err, "could not decrypt header")
|
|
}
|
|
|
|
return header, true, nil
|
|
}
|
|
|
|
return header, false, errors.Wrap(err, "could not decrypt header")
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) decodeHeader(
|
|
header []byte,
|
|
) (curves.Point, uint32, uint32, error) {
|
|
if len(header) < 9 {
|
|
return nil, 0, 0, errors.New("malformed header")
|
|
}
|
|
|
|
currentReceivingChainLength := binary.BigEndian.Uint32(header[len(header)-4:])
|
|
previousReceivingChainLength := binary.BigEndian.Uint32(
|
|
header[len(header)-8 : len(header)-4],
|
|
)
|
|
receivingEphemeralKeyBytes := header[:len(header)-8]
|
|
receivingEphemeralKey, err := r.curve.Point.FromAffineCompressed(
|
|
receivingEphemeralKeyBytes,
|
|
)
|
|
|
|
return receivingEphemeralKey,
|
|
previousReceivingChainLength,
|
|
currentReceivingChainLength,
|
|
errors.Wrap(err, "could not decode receiving dh key")
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) encrypt(
|
|
plaintext []byte,
|
|
key []byte,
|
|
associatedData []byte,
|
|
) (*protobufs.MessageCiphertext, error) {
|
|
iv := [12]byte{}
|
|
rand.Read(iv[:])
|
|
aesCipher, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not construct cipher")
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(aesCipher)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not construct block")
|
|
}
|
|
|
|
ciphertext := &protobufs.MessageCiphertext{}
|
|
|
|
if associatedData == nil {
|
|
associatedData = make([]byte, 32)
|
|
if _, err := rand.Read(associatedData); err != nil {
|
|
return nil, errors.Wrap(err, "could not obtain entropy")
|
|
}
|
|
ciphertext.AssociatedData = associatedData
|
|
}
|
|
|
|
ciphertext.Ciphertext = gcm.Seal(nil, iv[:], plaintext, associatedData)
|
|
ciphertext.InitializationVector = iv[:]
|
|
|
|
return ciphertext, nil
|
|
}
|
|
|
|
func (r *DoubleRatchetParticipant) decrypt(
|
|
ciphertext *protobufs.MessageCiphertext,
|
|
key []byte,
|
|
associatedData []byte,
|
|
) ([]byte, error) {
|
|
if associatedData == nil {
|
|
associatedData = ciphertext.AssociatedData
|
|
}
|
|
|
|
aesCipher, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not construct cipher")
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(aesCipher)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not construct block")
|
|
}
|
|
|
|
plaintext, err := gcm.Open(
|
|
nil,
|
|
ciphertext.InitializationVector,
|
|
ciphertext.Ciphertext,
|
|
associatedData,
|
|
)
|
|
|
|
return plaintext, errors.Wrap(err, "could not decrypt ciphertext")
|
|
}
|
|
|
|
func ratchetKeys(inputKey []byte) ([]byte, []byte, []byte) {
|
|
buf := hmac.New(sha512.New, inputKey)
|
|
buf.Write([]byte{AEAD_KEY})
|
|
aeadKey := buf.Sum(nil)
|
|
buf.Reset()
|
|
buf.Write([]byte{MESSAGE_KEY})
|
|
messageKey := buf.Sum(nil)
|
|
buf.Reset()
|
|
buf.Write([]byte{CHAIN_KEY})
|
|
chainKey := buf.Sum(nil)
|
|
|
|
return chainKey[:32], messageKey[:32], aeadKey[:32]
|
|
}
|