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 PROTOCOL_VERSION = 1 const PROTOCOL = 1<<8 + 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: 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) r.receivingChainKey = newChainKey r.currentReceivingChainLength++ plaintext, err = r.decrypt( envelope.MessageBody, messageKey, append( append([]byte{}, aeadKey...), envelope.MessageHeader.Ciphertext..., ), ) 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] }