From e9c9e86d92eda68cf69e5de92a96178ec9a893269894295790070a3e8810fce7 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Fri, 28 Feb 2025 16:53:47 +0300 Subject: [PATCH] Revised key commitment --- go/cm/enc/chapoly/dem.go | 126 +++++++++++++++++++++------------- spec/cm/dem-chapoly-krkc.texi | 17 ++--- 2 files changed, 86 insertions(+), 57 deletions(-) diff --git a/go/cm/enc/chapoly/dem.go b/go/cm/enc/chapoly/dem.go index 854a84d..220693d 100644 --- a/go/cm/enc/chapoly/dem.go +++ b/go/cm/enc/chapoly/dem.go @@ -19,6 +19,7 @@ import ( "bytes" "crypto/cipher" "crypto/hkdf" + "crypto/subtle" "errors" "hash" "io" @@ -35,12 +36,16 @@ const ( DEMAlgo = "chapoly-krkc" ) +type keymat struct { + key []byte + iv []byte +} + type job struct { - bufReady chan struct{} - processed chan struct{} - keyAndCommitment []byte - buf []byte - tail bool + bufReady chan struct{} + processed chan error + keymat keymat + buf []byte } func blake2bHash() hash.Hash { @@ -63,19 +68,26 @@ func do( } ready := make(chan *job, procs) dones := make(chan *job, procs) - keyAndCommitments := make(chan []byte, procs) + keymats := make(chan keymat, procs) go func() { ck := cek - var keyAndCommitment []byte + var key []byte + var iv []byte var errHKDF error for { - keyAndCommitment, errHKDF = hkdf.Expand( - blake2bHash, ck, "cm/encrypted/chapoly-krkc", - chacha20poly1305.KeySize+CommitmentLen) + key, errHKDF = hkdf.Expand( + blake2bHash, ck, "cm/encrypted/chapoly-krkc/key", + chacha20poly1305.KeySize) + if errHKDF != nil { + panic(errHKDF) + } + iv, errHKDF = hkdf.Expand( + blake2bHash, ck, "cm/encrypted/chapoly-krkc/iv", + chacha20poly1305.NonceSizeX) if errHKDF != nil { panic(errHKDF) } - keyAndCommitments <- keyAndCommitment + keymats <- keymat{key: key, iv: iv} ck, errHKDF = hkdf.Extract(blake2bHash, nil, ck) if errHKDF != nil { panic(errHKDF) @@ -96,56 +108,61 @@ func do( if err != nil { return } - var errUnauth error for range procs { go func() { - var ciph cipher.AEAD j := job{ buf: make([]byte, blobChunkLen), bufReady: make(chan struct{}), - processed: make(chan struct{}), + processed: make(chan error), } - nonce := make([]byte, chacha20poly1305.NonceSize) ready <- &j - var errOpen error + var errJob error + var ciph cipher.AEAD + var com hash.Hash + var tag []byte + ourCom := make([]byte, CommitmentLen) for { <-j.bufReady - ciph, err = chacha20poly1305.New( - j.keyAndCommitment[:chacha20poly1305.KeySize], - ) - if err != nil { - panic(err) + ciph, errJob = chacha20poly1305.NewX(j.keymat.key) + if errJob != nil { + panic(errJob) } - if j.tail { - nonce[len(nonce)-1] = 0x01 + com, errJob = blake2b.New(CommitmentLen, nil) + if errJob != nil { + panic(errJob) } if seal { ciph.Seal( j.buf[:0], - nonce, + j.keymat.iv, j.buf[:len(j.buf)-chacha20poly1305.Overhead-CommitmentLen], nil, ) - copy( - j.buf[len(j.buf)-CommitmentLen:], - j.keyAndCommitment[chacha20poly1305.KeySize:], - ) + tag = j.buf[len(j.buf)-chacha20poly1305.Overhead-CommitmentLen:] + tag = tag[:chacha20poly1305.Overhead] + com.Write(j.keymat.key) + com.Write(j.keymat.iv) + com.Write(tag) + com.Sum(j.buf[:len(j.buf)-CommitmentLen]) } else { - if bytes.Equal( + tag = j.buf[len(j.buf)-chacha20poly1305.Overhead-CommitmentLen:] + tag = tag[:chacha20poly1305.Overhead] + com.Write(j.keymat.key) + com.Write(j.keymat.iv) + com.Write(tag) + ourCom = com.Sum(ourCom[:0]) + if subtle.ConstantTimeCompare( + ourCom, j.buf[len(j.buf)-CommitmentLen:], - j.keyAndCommitment[chacha20poly1305.KeySize:], - ) { - j.buf, errOpen = ciph.Open( - j.buf[:0], nonce, j.buf[:len(j.buf)-CommitmentLen], nil, + ) == 1 { + j.buf, errJob = ciph.Open( + j.buf[:0], j.keymat.iv, j.buf[:len(j.buf)-CommitmentLen], nil, ) - if errOpen == nil { - errUnauth = errOpen - } } else { - errUnauth = errors.New("commitment differs") + errJob = errors.New("commitment differs") } } - j.processed <- struct{}{} + j.processed <- errJob } }() } @@ -154,7 +171,12 @@ func do( go func() { var n int64 for j := range dones { - <-j.processed + errW = <-j.processed + if errW != nil { + j.buf = j.buf[:blobChunkLen] + ready <- j + break + } if seal { if blob { n, errW = keks.BinEncode(w, j.buf) @@ -182,12 +204,13 @@ func do( var eof bool var n int var j *job + var tailMet bool for !eof { - if errUnauth != nil { - err = errUnauth - close(dones) - <-finished + select { + case <-finished: + err = errW return + default: } j = <-ready if seal { @@ -220,19 +243,24 @@ func do( if !seal && n == 0 { break } + j.keymat = <-keymats if (seal && n < ChunkLen) || (!seal && n < blobChunkLen) { - j.tail = true + j.keymat.iv[chacha20poly1305.NonceSizeX-1] |= 0x01 + tailMet = true + } else { + j.keymat.iv[chacha20poly1305.NonceSizeX-1] &= 0xFE } - j.keyAndCommitment = <-keyAndCommitments j.bufReady <- struct{}{} dones <- j } close(dones) <-finished - if errUnauth == nil { - err = errW - } else { - err = errUnauth + err = errW + if err != nil { + return + } + if !tailMet { + err = errors.New("no tail met") } return } diff --git a/spec/cm/dem-chapoly-krkc.texi b/spec/cm/dem-chapoly-krkc.texi index 67ad95f..87783b2 100644 --- a/spec/cm/dem-chapoly-krkc.texi +++ b/spec/cm/dem-chapoly-krkc.texi @@ -11,16 +11,17 @@ Data is split on 128 KiB chunks, each of which is encrypted the following way: @verbatim CK0 = CEK CKi = HKDF-Extract(BLAKE2b, salt="", ikm=CK{i-1}) -KEY || COMMITMENT = HKDF-Expand(BLAKE2b, prk=CKi, info="cm/encrypted/chapoly-krkc") -ChaCha20-Poly1305(key=KEY, ad="", nonce=11*0x00 || tail-flag, data=chunk) || COMMITMENT +KEY = HKDF-Expand(BLAKE2b, prk=CKi, info="cm/encrypted/chapoly-krkc/key") +IV = HKDF-Expand(BLAKE2b, prk=CKi, info="cm/encrypted/chapoly-krkc/iv", len=24) +if last chunk { IV[23] |= 0x01 } else { IV[23] &= 0xFE } +CIPHERTEXT || TAG = XChaCha20-Poly1305(key=KEY, ad="", nonce=IV, data=chunk) +COMMITMENT = BLAKE2b-256(KEY || IV || TAG) +CIPHERTEXT || TAG || COMMITMENT @end verbatim -Chaining key (CK) advances with every chunk. 256-bits encryption key and -key commitment are derived from the chaining key. +Chaining key (CK) advances with every chunk. 256-bit encryption key and +randomised 192-bit nonce (initialisation vector) are derived from it. -@code{tail-flag} is a byte indicating if that is the last chunk in the -payload. It equals to 0x01 for the last chunk and to 0x00 for other -ones. Last chunk should be smaller than previous ones, maybe (payload) -even empty. +Nonce's lowest bit is set only if this is the last chunk we encrypting. @code{/payload}'s chunk length equals to 128KiB+16+32 bytes. -- 2.48.1