]> Cypherpunks repositories - keks.git/commitdiff
Another key rotation/ratcheting/commitment revise
authorSergey Matveev <stargrave@stargrave.org>
Mon, 24 Feb 2025 11:07:39 +0000 (14:07 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Mon, 24 Feb 2025 12:48:03 +0000 (15:48 +0300)
18 files changed:
go/cm/cmd/enctool/main.go
go/cm/cmd/enctool/passphrase.t
go/cm/cmd/enctool/prv-encrypted.t
go/cm/enc/balloon/decap.go
go/cm/enc/chapoly/dem.go
go/cm/enc/chapoly/keywrap.go [new file with mode: 0644]
go/cm/enc/dem.go
go/cm/enc/enc.go
spec/cm/dem-chapoly-krkc.texi [new file with mode: 0644]
spec/cm/dem-kuznechik-ctr-hmac-kr.texi [new file with mode: 0644]
spec/cm/encrypted.cddl
spec/cm/encrypted.texi
spec/cm/kem-balloon-blake2b-hkdf.texi [new file with mode: 0644]
spec/cm/kem-gost3410-hkdf.texi [new file with mode: 0644]
spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi [new file with mode: 0644]
spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi [new file with mode: 0644]
spec/cm/keywrap-kexp15.texi [new file with mode: 0644]
spec/cm/keywrap-xchapoly.texi [new file with mode: 0644]

index 3dbf2d3b5d0d6675585e20b475c83c5b2ada2048d16e790fd5faddcfe069a343..72f0aa5126f6899f344a582d2d19cbb76251c4dfc051d81c5660f057cd11d9df 100644 (file)
@@ -33,6 +33,7 @@ import (
        "github.com/google/uuid"
        "go.cypherpunks.su/balloon/v3"
        "golang.org/x/crypto/blake2b"
+       "golang.org/x/crypto/chacha20poly1305"
        "golang.org/x/term"
 
        "go.cypherpunks.su/keks"
@@ -40,7 +41,7 @@ import (
        cmenc "go.cypherpunks.su/keks/cm/enc"
        cmballoon "go.cypherpunks.su/keks/cm/enc/balloon"
        ballooncost "go.cypherpunks.su/keks/cm/enc/balloon/cost"
-       chaPoly "go.cypherpunks.su/keks/cm/enc/chapoly"
+       chapoly "go.cypherpunks.su/keks/cm/enc/chapoly"
        mceliece6960119x25519 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519"
        mceliece6960119 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519/mceliece6960119"
        sntrup4591761x25519 "go.cypherpunks.su/keks/cm/enc/sntrup4591761-x25519"
@@ -100,7 +101,7 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) {
                                return
                        }
                }
-               if encrypted.DEM.A != cmenc.ChaCha20Poly1305 {
+               if encrypted.DEM.A != chapoly.DEMAlgo {
                        err = errors.New("unsupported prv encryption DEM")
                        return
                }
@@ -112,16 +113,12 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) {
                }
                passwd := readPasswd("Passphrase for private key:")
                var cek []byte
-               cek, err = cmballoon.Decapsulate(
-                       encrypted.KEM[0],
-                       encrypted.Salt[:],
-                       passwd,
-               )
+               cek, err = cmballoon.Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd)
                if err != nil {
                        return
                }
                var buf bytes.Buffer
-               _, err = chaPoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1)
+               _, err = chapoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1)
                if err != nil {
                        return
                }
@@ -144,7 +141,7 @@ func parsePrv(data []byte) (av cm.AV, tail []byte, err error) {
 func main() {
        log.SetFlags(log.Lshortfile)
        flag.Usage = usage
-       setSalt := flag.String("salt", "", "Set that /salt instead of autogeneration")
+       setSalt := flag.String("id", "", "Set that /id instead of autogeneration")
        includeTo := flag.Bool("include-to", false, `Include "to" field in KEMs`)
        passphrase := flag.Bool("p", false, "Use passphrase")
        balloonS := flag.Int("balloon-s", 1<<17, "Balloon's space cost")
@@ -152,7 +149,7 @@ func main() {
        balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads")
        doDecrypt := flag.Bool("d", false, "Decrypt")
        parallel := flag.Int("parallel", cmhash.DefaultNumCPU, "Parallel cryptors")
-       noblob := flag.Bool("no-stream", false, "Include payload into container")
+       noblob := flag.Bool("embed", false, "Include payload into container")
        flag.Parse()
 
        fdPubR := os.NewFile(FdPubR, "pub-in")
@@ -224,7 +221,7 @@ func main() {
                                log.Fatal(err)
                        }
                }
-               if encrypted.DEM.A != cmenc.ChaCha20Poly1305 {
+               if encrypted.DEM.A != chapoly.DEMAlgo {
                        log.Fatalln("unsupported DEM:", encrypted.DEM.A)
                }
                if len(encrypted.KEM) == 0 {
@@ -238,16 +235,12 @@ func main() {
                                        continue
                                }
                                passwd := readPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:")
-                               cek, err = cmballoon.Decapsulate(
-                                       kem,
-                                       encrypted.Salt[:],
-                                       passwd,
-                               )
+                               cek, err = cmballoon.Decapsulate(kem, encrypted.Id[:], passwd)
                                if err != nil {
                                        log.Print(err)
                                        continue
                                }
-                               if len(cek) != chaPoly.KeyLen {
+                               if len(cek) != chapoly.CEKLen {
                                        log.Println(kemIdx, kem.A, "wrong key len, skipping")
                                        continue
                                }
@@ -318,24 +311,24 @@ func main() {
                                                        prk,
                                                        string(append(
                                                                []byte(cmenc.SNTRUP4591761X25519Info),
-                                                               encrypted.Salt[:]...,
+                                                               encrypted.Id[:]...,
                                                        )),
-                                                       chaPoly.KeyLen,
+                                                       chacha20poly1305.KeySize,
                                                )
                                                if err != nil {
                                                        log.Fatal(err)
                                                }
-                                               var cekp bytes.Buffer
-                                               _, err = chaPoly.Open(&cekp, bytes.NewReader(kem.CEK), kek, 1)
+                                               var cekp []byte
+                                               cekp, err = chapoly.Unwrap(kek, kem.CEK)
                                                if err != nil {
                                                        log.Println(kemIdx, kem.A, err, ", skipping")
                                                        continue
                                                }
-                                               if cekp.Len() != chaPoly.KeyLen {
+                                               if len(cekp) != chapoly.CEKLen {
                                                        log.Println(kemIdx, kem.A, "wrong key len, skipping")
                                                        continue
                                                }
-                                               cek = cekp.Bytes()
+                                               cek = cekp
                                                break
                                        }
                                }
@@ -414,24 +407,24 @@ func main() {
                                                        prk,
                                                        string(append(
                                                                []byte(cmenc.ClassicMcEliece6960119X25519Info),
-                                                               encrypted.Salt[:]...,
+                                                               encrypted.Id[:]...,
                                                        )),
-                                                       chaPoly.KeyLen,
+                                                       chacha20poly1305.KeySize,
                                                )
                                                if err != nil {
                                                        log.Fatal(err)
                                                }
-                                               var cekp bytes.Buffer
-                                               _, err = chaPoly.Open(&cekp, bytes.NewReader(kem.CEK), kek, 1)
+                                               var cekp []byte
+                                               cekp, err = chapoly.Unwrap(kek, kem.CEK)
                                                if err != nil {
                                                        log.Println(kemIdx, kem.A, err, ", skipping")
                                                        continue
                                                }
-                                               if cekp.Len() != chaPoly.KeyLen {
+                                               if len(cekp) != chapoly.CEKLen {
                                                        log.Println(kemIdx, kem.A, "wrong key len, skipping")
                                                        continue
                                                }
-                                               cek = cekp.Bytes()
+                                               cek = cekp
                                                break
                                        }
                                }
@@ -447,25 +440,25 @@ func main() {
                        log.Fatal("no KEMs processed")
                }
                if len(encrypted.Payload) > 0 {
-                       _, err = chaPoly.Open(os.Stdout, bytes.NewReader(encrypted.Payload), cek, *parallel)
+                       _, err = chapoly.Open(os.Stdout, bytes.NewReader(encrypted.Payload), cek, *parallel)
                } else {
-                       _, err = chaPoly.OpenBlob(os.Stdout, os.Stdin, cek, *parallel)
+                       _, err = chapoly.OpenBlob(os.Stdout, os.Stdin, cek, *parallel)
                }
                if err != nil {
                        log.Fatal(err)
                }
        } else {
-               var salt uuid.UUID
+               var id uuid.UUID
                if *setSalt == "" {
-                       salt, err = uuid.NewRandom()
+                       id, err = uuid.NewRandom()
                } else {
-                       salt, err = uuid.Parse(*setSalt)
+                       id, err = uuid.Parse(*setSalt)
                }
                if err != nil {
                        log.Fatal(err)
                }
                var kems []cmenc.KEM
-               cek = make([]byte, chaPoly.KeyLen)
+               cek = make([]byte, chapoly.CEKLen)
                rand.Read(cek)
                if *passphrase {
                        passwd := readPasswd("Passphrase:")
@@ -475,11 +468,11 @@ func main() {
                                        log.Fatal("passphrases do not match")
                                }
                        }
-                       bSalt := make([]byte, cmballoon.SaltLen)
-                       rand.Read(bSalt)
+                       salt := make([]byte, cmballoon.SaltLen)
+                       rand.Read(salt)
                        kem := cmenc.KEM{
                                A:    cmballoon.BalloonBLAKE2bHKDF,
-                               Salt: bSalt,
+                               Salt: salt,
                                BalloonCost: &ballooncost.Cost{
                                        S: uint64(*balloonS),
                                        T: uint64(*balloonT),
@@ -490,19 +483,19 @@ func main() {
                                var kek []byte
                                kek, err = hkdf.Expand(
                                        blake2bHash,
-                                       balloon.H(blake2bHash, passwd, bSalt, *balloonS, *balloonT, *balloonP),
-                                       string(append([]byte(cmballoon.HKDFInfo), salt[:]...)),
-                                       chaPoly.KeyLen,
+                                       balloon.H(blake2bHash, passwd, salt, *balloonS, *balloonT, *balloonP),
+                                       string(append([]byte(cmballoon.HKDFInfo), id[:]...)),
+                                       chacha20poly1305.KeySize,
                                )
                                if err != nil {
                                        log.Fatal(err)
                                }
-                               var cekp bytes.Buffer
-                               _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1)
+                               var cekp []byte
+                               cekp, err = chapoly.Wrap(kek, cek)
                                if err != nil {
                                        log.Fatal(err)
                                }
-                               kem.CEK = cekp.Bytes()
+                               kem.CEK = cekp
                        }
                        kems = append(kems, kem)
                }
@@ -558,18 +551,18 @@ func main() {
                                        kek, err = hkdf.Expand(
                                                blake2bHash,
                                                prk,
-                                               string(append([]byte(cmenc.SNTRUP4591761X25519Info), salt[:]...)),
-                                               chaPoly.KeyLen,
+                                               string(append([]byte(cmenc.SNTRUP4591761X25519Info), id[:]...)),
+                                               chacha20poly1305.KeySize,
                                        )
                                        if err != nil {
                                                log.Fatal(err)
                                        }
-                                       var cekp bytes.Buffer
-                                       _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1)
+                                       var cekp []byte
+                                       cekp, err = chapoly.Wrap(kek, cek)
                                        if err != nil {
                                                log.Fatal(err)
                                        }
-                                       kem.CEK = cekp.Bytes()
+                                       kem.CEK = cekp
                                }
                                if *includeTo {
                                        kem.To = pubIds[pubId]
@@ -626,18 +619,18 @@ func main() {
                                        kek, err = hkdf.Expand(
                                                cmhash.NewSHAKE256,
                                                prk,
-                                               string(append([]byte(cmenc.ClassicMcEliece6960119X25519Info), salt[:]...)),
-                                               chaPoly.KeyLen,
+                                               string(append([]byte(cmenc.ClassicMcEliece6960119X25519Info), id[:]...)),
+                                               chacha20poly1305.KeySize,
                                        )
                                        if err != nil {
                                                log.Fatal(err)
                                        }
-                                       var cekp bytes.Buffer
-                                       _, err = chaPoly.Seal(&cekp, bytes.NewReader(cek), kek, 1)
+                                       var cekp []byte
+                                       cekp, err = chapoly.Wrap(kek, cek)
                                        if err != nil {
                                                log.Fatal(err)
                                        }
-                                       kem.CEK = cekp.Bytes()
+                                       kem.CEK = cekp
                                }
                                if *includeTo {
                                        kem.To = pubIds[pubId]
@@ -656,13 +649,13 @@ func main() {
                                log.Fatal(err)
                        }
                        enc := cmenc.Encrypted{
-                               Salt: salt,
-                               KEM:  kems,
-                               DEM:  cmenc.DEM{A: cmenc.ChaCha20Poly1305},
+                               Id:  id,
+                               KEM: kems,
+                               DEM: cmenc.DEM{A: chapoly.DEMAlgo},
                        }
                        if *noblob {
                                var buf bytes.Buffer
-                               if _, err = chaPoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil {
+                               if _, err = chapoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil {
                                        log.Fatal(err)
                                }
                                enc.Payload = buf.Bytes()
@@ -675,7 +668,7 @@ func main() {
                        }
                }
                if !*noblob {
-                       if _, err = chaPoly.SealBlob(os.Stdout, os.Stdin, cek, *parallel); err != nil {
+                       if _, err = chapoly.SealBlob(os.Stdout, os.Stdin, cek, *parallel); err != nil {
                                log.Fatal(err)
                        }
                }
index f738873faa88236b8aaa882eddbb7fbe9d74b711e056cc566f1ebe0ea2b6b69b..9ef833ffe9a5863838630c4108b4f1b31f3f44b6a1357e726a2638e676189cba 100755 (executable)
@@ -10,7 +10,7 @@ export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd
 balloonparams="-balloon-s 123 -balloon-t 2"
 test_expect_success "encrypting" "cmenctool $balloonparams -p \
     <$TMPDIR/enc.data >$TMPDIR/enc.enc"
-test_expect_success "decrypting" "cmenctool $balloonparams -d -p \
+test_expect_success "decrypting" "cmenctool -d -p \
     <$TMPDIR/enc.enc >$TMPDIR/enc.data.got"
 test_expect_success "comparing" \
     "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got"
index 1e83d9f10505a740e459fa5f950a949488a257e5ce31aca776060868fe77d75e..59f8c99ada86ad6493021600cc26de66d42bd490ef631ff94a9c47e3fceaa2c7 100755 (executable)
@@ -9,7 +9,7 @@ cmkeytool -algo sntrup4591761-x25519 -ku kem -sub A=KEY 5>$TMPDIR/enc.pub 9>$TMP
 dd if=/dev/urandom of=$TMPDIR/enc.data bs=12K count=1 2>/dev/null
 export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p)
 balloonparams="-balloon-s 123 -balloon-t 2"
-test_expect_success "key encrypting" "cmenctool -p -no-stream $balloonparams \
+test_expect_success "key encrypting" "cmenctool -p -embed $balloonparams \
     <$TMPDIR/enc.prv >$TMPDIR/enc.prv.enc"
 test_expect_success "data encrypting" "cmenctool 4<$TMPDIR/enc.pub \
     <$TMPDIR/enc.data >$TMPDIR/enc.enc"
index b37acffb6a74bfb2e3a9b5687223085299448ac06487bad10dd52cd19c547eb1..56a3c0cd30c27adc1a3935dee361d859332657760cc93f2479a15a2312051524 100644 (file)
@@ -16,7 +16,6 @@
 package balloon
 
 import (
-       "bytes"
        "crypto/hkdf"
        "errors"
        "hash"
@@ -25,6 +24,7 @@ import (
        cmenc "go.cypherpunks.su/keks/cm/enc"
        chaPoly "go.cypherpunks.su/keks/cm/enc/chapoly"
        "golang.org/x/crypto/blake2b"
+       "golang.org/x/crypto/chacha20poly1305"
 )
 
 const (
@@ -41,7 +41,7 @@ func blake2bHash() hash.Hash {
        return h
 }
 
-func Decapsulate(kem cmenc.KEM, encSalt, passphrase []byte) (cek []byte, err error) {
+func Decapsulate(kem cmenc.KEM, id, passphrase []byte) (cek []byte, err error) {
        if len(kem.Salt) == 0 {
                return nil, errors.New("missing salt")
        }
@@ -59,13 +59,11 @@ func Decapsulate(kem cmenc.KEM, encSalt, passphrase []byte) (cek []byte, err err
                        int(kem.BalloonCost.T),
                        int(kem.BalloonCost.P),
                ),
-               string(append([]byte(HKDFInfo), encSalt...)),
-               chaPoly.KeyLen,
+               string(append([]byte(HKDFInfo), id...)),
+               chacha20poly1305.KeySize,
        )
        if err != nil {
                return nil, err
        }
-       var buf bytes.Buffer
-       _, err = chaPoly.Open(&buf, bytes.NewReader(kem.CEK), kek, 1)
-       return buf.Bytes(), err
+       return chaPoly.Unwrap(kek, kem.CEK)
 }
index d9d2700baa67ff67c078ff354ca79883410150aa61cbef7a63db2dd6fc0b5bfc..b23888e1a6f697b34cb359d83e8bdd726488eb16cc6415244874d543878c5197 100644 (file)
 // You should have received a copy of the GNU Lesser General Public
 // License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-package dem
+package chapoly
 
 import (
        "bytes"
        "crypto/cipher"
-       "crypto/subtle"
+       "crypto/hkdf"
        "errors"
+       "hash"
        "io"
 
        "go.cypherpunks.su/keks"
+       "golang.org/x/crypto/blake2b"
        "golang.org/x/crypto/chacha20poly1305"
 )
 
 const (
-       ChunkLen = 128 * 1024
-       PadLen   = 32
-       KeyLen   = chacha20poly1305.KeySize + chacha20poly1305.NonceSize
+       ChunkLen      = 128 * 1024
+       CommitmentLen = 32
+       CEKLen        = blake2b.Size
+       DEMAlgo       = "chapoly-krkc"
 )
 
-func incr(data []byte) {
-       for i := len(data) - 1; i >= 0; i-- {
-               data[i]++
-               if data[i] != 0 {
-                       return
-               }
-       }
+type job struct {
+       bufReady         chan struct{}
+       processed        chan struct{}
+       keyAndCommitment []byte
+       buf              []byte
+       tail             bool
 }
 
-type job struct {
-       bufReady  chan struct{}
-       processed chan struct{}
-       buf       []byte
-       nonce     []byte
+func blake2bHash() hash.Hash {
+       h, err := blake2b.New512(nil)
+       if err != nil {
+               panic(err)
+       }
+       return h
 }
 
 func do(
        w io.Writer,
        blob, seal bool,
        r io.Reader,
-       key []byte,
+       cek []byte,
        procs int,
 ) (total int64, err error) {
-       if len(key) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize {
+       if len(cek) != CEKLen {
                return 0, errors.New("wrong CEK len")
        }
        ready := make(chan *job, procs)
        dones := make(chan *job, procs)
-       var iv []byte
-       key, iv = key[:chacha20poly1305.KeySize], key[chacha20poly1305.KeySize:]
-       var ctr []byte
-       var overhead int
-       {
-               var ciph cipher.AEAD
-               ciph, err = chacha20poly1305.New(key)
-               if err != nil {
-                       return 0, err
+       keyAndCommitments := make(chan []byte, procs)
+       go func() {
+               ck := cek
+               var keyAndCommitment []byte
+               var errHKDF error
+               for {
+                       keyAndCommitment, errHKDF = hkdf.Expand(
+                               blake2bHash, ck, "dem-chapoly-krkc",
+                               chacha20poly1305.KeySize+CommitmentLen)
+                       if errHKDF != nil {
+                               panic(errHKDF)
+                       }
+                       keyAndCommitments <- keyAndCommitment
+                       ck, errHKDF = hkdf.Extract(blake2bHash, nil, ck)
+                       if errHKDF != nil {
+                               panic(errHKDF)
+                       }
                }
-               ctr = make([]byte, ciph.NonceSize())
-               overhead = ciph.Overhead()
-       }
-       blobChunkLen := PadLen + ChunkLen + overhead
+       }()
+       blobChunkLen := ChunkLen + chacha20poly1305.Overhead + CommitmentLen
        var blobDecoder *keks.BlobDecoder
        if seal {
                if blob {
@@ -87,36 +96,53 @@ func do(
        if err != nil {
                return
        }
-       chaPolyPad := make([]byte, PadLen)
        var errUnauth error
        for range procs {
                go func() {
                        var ciph cipher.AEAD
-                       ciph, err = chacha20poly1305.New(key)
-                       if err != nil {
-                               panic(err)
-                       }
                        j := job{
                                buf:       make([]byte, blobChunkLen),
-                               nonce:     make([]byte, ciph.NonceSize()),
                                bufReady:  make(chan struct{}),
                                processed: make(chan struct{}),
                        }
-                       nonce := make([]byte, ciph.NonceSize())
+                       nonce := make([]byte, chacha20poly1305.NonceSize)
                        ready <- &j
                        var errOpen error
                        for {
                                <-j.bufReady
+                               ciph, err = chacha20poly1305.New(
+                                       j.keyAndCommitment[:chacha20poly1305.KeySize],
+                               )
+                               if err != nil {
+                                       panic(err)
+                               }
+                               if j.tail {
+                                       nonce[len(nonce)-1] = 0x01
+                               }
                                if seal {
-                                       ciph.Seal(j.buf[:0], nonce, j.buf[:len(j.buf)-overhead], nil)
+                                       ciph.Seal(
+                                               j.buf[:0],
+                                               nonce,
+                                               j.buf[:len(j.buf)-chacha20poly1305.Overhead-CommitmentLen],
+                                               nil,
+                                       )
+                                       copy(
+                                               j.buf[len(j.buf)-CommitmentLen:],
+                                               j.keyAndCommitment[chacha20poly1305.KeySize:],
+                                       )
                                } else {
-                                       j.buf, errOpen = ciph.Open(j.buf[:0], nonce, j.buf, nil)
-                                       if errOpen == nil {
-                                               if subtle.ConstantTimeCompare(j.buf[:PadLen], chaPolyPad) != 1 {
-                                                       errUnauth = errors.New("bad pad")
+                                       if bytes.Equal(
+                                               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,
+                                               )
+                                               if errOpen == nil {
+                                                       errUnauth = errOpen
                                                }
                                        } else {
-                                               errUnauth = errOpen
+                                               errUnauth = errors.New("commitment differs")
                                        }
                                }
                                j.processed <- struct{}{}
@@ -139,7 +165,7 @@ func do(
                                if len(j.buf) == 0 {
                                        n = 0
                                } else {
-                                       n, errW = io.Copy(w, bytes.NewReader(j.buf[PadLen:]))
+                                       n, errW = io.Copy(w, bytes.NewReader(j.buf))
                                }
                        }
                        total += n
@@ -165,7 +191,7 @@ func do(
                }
                j = <-ready
                if seal {
-                       n, errR = io.ReadFull(r, j.buf[PadLen:PadLen+ChunkLen])
+                       n, errR = io.ReadFull(r, j.buf[:ChunkLen])
                } else {
                        if blobDecoder == nil {
                                n, errR = io.ReadFull(r, j.buf)
@@ -188,19 +214,16 @@ func do(
                        errR = nil
                        eof = true
                        if seal {
-                               j.buf = j.buf[:PadLen+n+overhead]
+                               j.buf = j.buf[:n+chacha20poly1305.Overhead+CommitmentLen]
                        }
                }
-               if seal {
-                       clear(j.buf[:PadLen])
-               } else if n == 0 {
+               if !seal && n == 0 {
                        break
                }
                if (seal && n < ChunkLen) || (!seal && n < blobChunkLen) {
-                       ctr[len(ctr)-1] = 0x01
+                       j.tail = true
                }
-               subtle.XORBytes(j.nonce, ctr, iv)
-               incr(ctr[:len(ctr)-1])
+               j.keyAndCommitment = <-keyAndCommitments
                j.bufReady <- struct{}{}
                dones <- j
        }
diff --git a/go/cm/enc/chapoly/keywrap.go b/go/cm/enc/chapoly/keywrap.go
new file mode 100644 (file)
index 0000000..eb623ea
--- /dev/null
@@ -0,0 +1,49 @@
+// GoKEKS/CM -- KEKS-encoded cryptographic messages
+// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, version 3 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package chapoly
+
+import (
+       "crypto/rand"
+       "errors"
+
+       "golang.org/x/crypto/chacha20poly1305"
+)
+
+func Wrap(kek, cek []byte) ([]byte, error) {
+       nonce := make([]byte, chacha20poly1305.NonceSizeX)
+       rand.Reader.Read(nonce)
+       ciph, err := chacha20poly1305.NewX(kek)
+       if err != nil {
+               return nil, err
+       }
+       return append(nonce, ciph.Seal(nil, nonce, cek, nil)...), nil
+}
+
+func Unwrap(kek, encap []byte) ([]byte, error) {
+       if len(encap) <= chacha20poly1305.NonceSizeX+chacha20poly1305.Overhead {
+               return nil, errors.New("encap is too small")
+       }
+       ciph, err := chacha20poly1305.NewX(kek)
+       if err != nil {
+               return nil, err
+       }
+       return ciph.Open(
+               nil,
+               encap[:chacha20poly1305.NonceSizeX],
+               encap[chacha20poly1305.NonceSizeX:],
+               nil,
+       )
+}
index 0f289b2caf614422ac558b57d6600bd872800c2743639364d6f854a82c9c6967..974f00826bea49a465525d013895ad307c84f64e968787ba530c55a4a3289401 100644 (file)
@@ -1,7 +1,5 @@
 package encrypted
 
-const ChaCha20Poly1305 = "chacha20poly1305"
-
 type DEM struct {
        A string `keks:"a"`
 }
index 4ec157d8d6dbbac78614d60978ff2e0bfe9d855006ee3f6fb1dd75689542ad8e..effe61226cd356650f3477d7b37b4af7d99b07aa02bc96c361982f41627d7c8e 100644 (file)
@@ -6,5 +6,5 @@ type Encrypted struct {
        DEM     DEM       `keks:"dem"`
        KEM     []KEM     `keks:"kem"`
        Payload []byte    `keks:"payload,omitempty"`
-       Salt    uuid.UUID `keks:"salt"`
+       Id      uuid.UUID `keks:"id"`
 }
diff --git a/spec/cm/dem-chapoly-krkc.texi b/spec/cm/dem-chapoly-krkc.texi
new file mode 100644 (file)
index 0000000..c3700a8
--- /dev/null
@@ -0,0 +1,26 @@
+@node dem-chapoly-krkc
+@cindex dem-chapoly-krkc
+@nodedescription ChaCha20-Poly1305 with key ratcheting and key commitment DEM
+@subsubsection ChaCha20-Poly1305 with key ratcheting and key commitment DEM
+
+@code{cm/encrypted}'s @code{/dem/a} equals to "chapoly-krkc".
+
+CEK is 64 bytes long.
+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="dem-chapoly-krkc")
+ChaCha20-Poly1305(key=KEY, ad="", nonce=11*0x00 || tail-flag, data=chunk) || COMMITMENT
+@end verbatim
+
+Chaining key (CK) advances with every chunk. 256-bits encryption key and
+key commitment are derived from the chaining key.
+
+@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.
+
+@code{/payload}'s chunk length equals to 128KiB+16+32 bytes.
diff --git a/spec/cm/dem-kuznechik-ctr-hmac-kr.texi b/spec/cm/dem-kuznechik-ctr-hmac-kr.texi
new file mode 100644 (file)
index 0000000..1df0f05
--- /dev/null
@@ -0,0 +1,23 @@
+@node dem-kuznechik-ctr-hmac-kr
+@cindex dem-kuznechik-ctr-hmac-kr
+@nodedescription Kuznechik-CTR-HMAC with key ratcheting DEM
+@subsubsection Kuznechik-CTR-HMAC with key ratcheting DEM
+
+@code{cm/encrypted}'s @code{/dem/a} equals to "kuznechik-ctr-hmac-kr".
+
+CEK is 64 bytes long.
+Data is split on 128 KiB chunks, each of which is encrypted the following way:
+
+@verbatim
+CK0 = CEK
+CKi = HKDF-Extract(Streebog-512, salt="", ikm=CK{i-1})
+Kenc || Kauth || KauthTail = HKDF-Expand(
+    Streebog-512, prk=CKi, info="dem-kuznechik-ctr-hmac-kr")
+CT = Kuznechik-CTR(key=Kenc, ctr=0x00, data=chunk)
+CT || HMAC(Streebog-256, key={Kauth|KauthTail}, data=CT)
+@end verbatim
+
+@code{KauthTail} is used only in the last chunk to explicitly signal
+that it is the last one.
+
+@code{/payload}'s chunk length equals to 128KiB+32 bytes.
index cdb65963c40f651ed9d03eca484db83a456b911b59cc622a2ad413ddc2e48771..4a888d787475509537a227f5a3dc51b14d0e1b056a02914e356c48b0b29ba5e9 100644 (file)
@@ -1,29 +1,20 @@
-ai = text ; algorithm identifier
-
 cm-encrypted = {
+    id: uuid,
     dem: dem,
     kem: [+ kem],
-    salt: uuid,
     ? payload: bytes,
 }
 
-dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac
+dem = dem-chapoly-krkc / dem-kuznechik-ctr-hmac-kr
 
-dem-chacha20poly1305 = {a: "chacha20poly1305"}
-dem-kuznechik-ctracpkm-hmac-hkdf = {a: "kuznechik-ctracpkm-hmac"}
+dem-chapoly-krkc = {a: "chapoly-krkc"}
+dem-kuznechik-ctr-hmac-kr = {a: "kuznechik-ctr-hmac-kr"}
 
-kem = kem-generic /
-      kem-balloon-blake2b-hkdf /
-      kem-gost3410-hkdf-kexp15 /
+kem = kem-balloon-blake2b-hkdf /
+      kem-gost3410-hkdf /
       kem-sntrup4591761-x25519-hkdf-blake2b /
       kem-mceliece6960119-x25519-hkdf-shake256
 
-kem-generic = {
-    a: ai,
-    cek: bytes,
-    * text => any
-}
-
 kem-balloon-blake2b-hkdf = {
     a: "balloon-blake2b-hkdf",
     cek: bytes,
index b7f4fc5fc5515a35fc6337e5cc1e31ec430ff32f9ba906adbbd2adba88f78c75..be7771b303d47dcd2ba00db3f407c33b641f9821a3c1a09575e06c187169a64f 100644 (file)
@@ -25,182 +25,33 @@ contains an encrypted CEK.
 If KEM uses public-key based cryptography, then recipient's
 @ref{cm-pub, public key}(s) should be provided, which may lack the
 signatures at all. Optional @code{/kem/*/to}, public key's fingerprint,
-may provide a hint for quickly searching for the key on the recipient's
-side.
+may provide a hint to quickly search for the key on the recipient's side.
 
-@code{/salt} is used in KEMs. UUIDv4 is recommended.
+@code{/id} is used in KEMs for domain separation. UUIDv4 is recommended.
+Can be null for privacy reasons.
 
-@node cm-encrypted-chacha20poly1305
-@cindex cm-encrypted-chacha20poly1305
-@nodedescription cm/encrypted with ChaCha20-Poly1305 DEM
-@subsection cm/encrypted with ChaCha20-Poly1305 DEM
+@node Key wrapping
+@cindex key wrapping
+@nodedescription Key wrapping mechanisms
+@subsection Key wrapping mechanisms
 
-    @code{/dem/a} equals to "chacha20poly1305".
+@include cm/keywrap-xchapoly.texi
+@include cm/keywrap-kexp15.texi
 
-    CEK is 32+12=44 bytes long and contains the key itself and
-    initialisation vector used in nonce.
+@node DEM
+@cindex DEM
+@nodedescription Data encapsulation mechanisms
+@subsection Data encapsulation mechanisms
 
-    Data is split on 128 KiB chunks which are encrypted the following way:
+@include cm/dem-chapoly-krkc.texi
+@include cm/dem-kuznechik-ctr-hmac-kr.texi
 
-@verbatim
-KEY || IV = CEK
-ChaCha20-Poly1305(key=KEY, ad="",
-    nonce=IV XOR (BE(11-byte counter) || tail-flag),
-    data=32*0x00 || chunk)
-@end verbatim
+@node KEM
+@cindex KEM
+@nodedescription Key encapsulation mechanisms
+@subsection Key encapsulation mechanisms
 
-    @code{counter} starts at zero and incremented with each chunk.
-
-    @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.
-
-    @code{/payload}'s chunk length equals to 32+128KiB+16 bytes.
-
-@node cm-encrypted-kuznechik-ctracpkm-hmac
-@cindex cm-encrypted-kuznechik-ctracpkm-hmac
-@nodedescription cm/encrypted with Kuznechik-CTR-ACPKM+HMAC DEM
-@subsection cm/encrypted with Kuznechik-CTR-ACPKM+HMAC DEM
-
-    @code{/dem/a} equals to "kuznechik-ctracpkm-hmac".
-    CEK is 32+8+32=72 bytes long.
-
-@verbatim
-Kenc || IV || Kauth = CEK
-@end verbatim
-
-    Encryption is performed with Kuznechik (ГОСТ Р 34.12-2015) block cipher
-    in CTR-ACPKM mode of operation (Р 1323565.1.017) with 256KiB section
-    size and IV initialisation vector. Authentication of ciphertext is
-    performed with Streebog-512 (ГОСТ Р 34.11-2012) in HMAC mode.
-
-    @code{/payload}'s chunk length equals to 128KiB bytes.
-
-@node cm-encrypted-balloon-blake2b-hkdf
-@cindex cm-encrypted-balloon-blake2b-hkdf
-@nodedescription cm/encrypted with Balloon-BLAKE2b+HKDF KEM
-@subsection cm/encrypted with Balloon-BLAKE2b+HKDF KEM
-
-    @code{/kem/*/a} equals to "balloon-blake2b-hkdf".
-    Recipient map must also contain additional fields:
-
-    @table @code
-    @item /kem/*/cost/s: uint64
-        Balloon's space cost (buffer size, number of hash-output sized blocks).
-    @item /kem/*/cost/t: uint64
-        Balloon's time cost (number of rounds).
-    @item /kem/*/cost/p: uint64
-        Balloon's parallel cost (number of threads).
-    @item /kem/*/salt: bin
-        Salt.
-    @end table
-
-    @url{https://crypto.stanford.edu/balloon/, Balloon} memory-hardened
-    password hasher must be used with BLAKE2b-256 hash.
-
-    @code{/kem/*/cek} is encrypted with
-    @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is
-    zero, tail-flag is set and CEK is KEK:
-
-@verbatim
-KEK = HKDF-Expand(BLAKE2b,
-    prk=balloon(BLAKE2b, passphrase, /kem/salt, s, t, p),
-    info="keks/cm/encrypted/balloon-blake2b-hkdf" || /salt)
-@end verbatim
-
-@node cm-encrypted-gost3410-hkdf-kexp15
-@cindex cm-encrypted-gost3410-hkdf-kexp15
-@nodedescription cm/encrypted with GOST R 34.10+HKDF+KExp15 KEM
-@subsection cm/encrypted with GOST R 34.10+HKDF+KExp15 KEM
-
-    @code{/kem/*/a} equals to "gost3410-hkdf-kexp15".
-    Recipient map must also contain additional fields:
-
-    @table @code
-    @item /to/*/ukm: bytes
-        Additional 16-bytes keying material.
-    @item /to/*/pub: bytes
-        Sender's ephemeral 512-bit public key.
-    @end table
-
-    ГОСТ Р 34.10-2012 VKO parameter set A/C ("gost3410-256A", "gost3410-512C")
-    must be used for DH operation, with UKM taken from the structure. VKO's
-    output is 512- or 1024-bit @code{BE(X)||BE(Y)} point. It is used in HKDF
-    and KExp15 (Р 1323565.1.017) key wrapping algorithm:
-
-@verbatim
-PRK = HKDF-Extract(Streebog-512, salt="", ikm=VKO(..., ukm=UKM))
-KEKenv, IV, KEKauth = HKDF-Expand(Streebog-512, prk=PRK,
-    info="keks/cm/encrypted/gost3410-hkdf-kexp15" || /salt)
-KExp15(KEKenc, KEKauth, IV, CEK) = CTR(Kenc, CEK || CMAC(Kauth, IV || CEK), IV=IV)
-@end verbatim
-
-@node cm-encrypted-sntrup4591761-x25519-hkdf-blake2b
-@cindex cm-encrypted-sntrup4591761-x25519-hkdf-blake2b
-@nodedescription cm/encrypted with SNTRUP4591761+X25519+HKDF-BLAKE2b KEM
-@subsection cm/encrypted with SNTRUP4591761+X25519+HKDF-BLAKE2b KEM
-
-    @code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b".
-    Recipient public key with
-    @ref{cm-pub-sntrup4591761-x25519, @code{sntrup4591761-x25519}}
-    algorithm must be used. It should have "kem" key usage set.
-
-    Recipient map must also contain additional field:
-    @code{/kem/*/encap: bytes} -- concatenation of 1047 bytes of Streamlined
-    NTRU Prime 4591^761's ciphertext with 32 bytes of ephemeral
-    X25519 public key.
-
-    Recipient performs X25519 and SNTRUP computation to
-    derive/decapsulate two 32-byte shared keys. Then it combines
-    them to get the KEK decryption key of the CEK.
-
-@verbatim
-PRK = HKDF-Extract(BLAKE2b, salt="", ikm=
-        sntrup4591761-sender-ciphertext ||
-        x25519-sender-public-key ||
-        sntrup4591761-recipient-public-key ||
-        x25519-recipient-public-key ||
-        sntrup4591761-shared-key ||
-        x25519-shared-key)
-KEK = HKDF-Expand(BLAKE2b, prk=PRK,
-    info="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" || /salt)
-@end verbatim
-
-    @code{/kem/*/cek} is encrypted with
-    @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is
-    zero, tail-flag is set and CEK is KEK.
-
-@node cm-encrypted-mceliece6960119-x25519-hkdf-shake256
-@cindex cm-encrypted-mceliece6960119-x25519-hkdf-shake256
-@nodedescription cm/encrypted with Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM
-@subsection cm/encrypted with Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM
-
-    @code{/kem/*/a} equals to "mceliece6960119-x25519-hkdf-shake256".
-    Recipient public key with
-    @ref{cm-pub-mceliece6960119-x25519, @code{mceliece6960119-x25519}}
-    algorithm must be used. It should have "kem" key usage set.
-
-    Recipient map must also contain additional field:
-    @code{/kem/*/encap: bytes} -- concatenation of 194 bytes of
-    Classic McEliece 6960-119 ciphertext with 32 bytes of ephemeral
-    X25519 public key.
-
-    Recipient performs X25519 and Classic McEliece computation to
-    derive/decapsulate two 32-byte shared keys. Then it combines
-    them to get the KEK decryption key of the CEK.
-
-@verbatim
-PRK = HKDF-Extract(SHAKE256, salt="", ikm=
-        mceliece6960119-sender-ciphertext ||
-        x25519-sender-public-key ||
-        mceliece6960119-recipient-public-key ||
-        x25519-recipient-public-key ||
-        mceliece6960119-shared-key ||
-        x25519-shared-key)[:32]
-KEK = HKDF-Expand(SHAKE256, prk=PRK,
-    info="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" || /salt)
-@end verbatim
-
-    @code{/kem/*/cek} is encrypted with
-    @ref{cm-encrypted-chacha20poly1305} algorithm, where counter is
-    zero, tail-flag is set and CEK is KEK.
+@include cm/kem-balloon-blake2b-hkdf.texi
+@include cm/kem-gost3410-hkdf.texi
+@include cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi
+@include cm/kem-mceliece6960119-x25519-hkdf-shake256.texi
diff --git a/spec/cm/kem-balloon-blake2b-hkdf.texi b/spec/cm/kem-balloon-blake2b-hkdf.texi
new file mode 100644 (file)
index 0000000..c87c5fa
--- /dev/null
@@ -0,0 +1,29 @@
+@node kem-balloon-blake2b-hkdf
+@cindex kem-balloon-blake2b-hkdf
+@nodedescription Balloon-BLAKE2b+HKDF KEM
+@subsubsection Balloon-BLAKE2b+HKDF KEM
+
+@code{/kem/*/a} equals to "balloon-blake2b-hkdf".
+Recipient map must also contain additional fields:
+
+@table @code
+@item /kem/*/cost/s: uint64
+    Balloon's space cost (buffer size, number of hash-output sized blocks).
+@item /kem/*/cost/t: uint64
+    Balloon's time cost (number of rounds).
+@item /kem/*/cost/p: uint64
+    Balloon's parallel cost (number of threads).
+@item /kem/*/salt: bytes
+    Salt.
+@end table
+
+@url{https://crypto.stanford.edu/balloon/, Balloon} memory-hardened
+password hasher must be used with BLAKE2b hash.
+
+@verbatim
+KEK = HKDF-Expand(BLAKE2b,
+    prk=balloon(BLAKE2b, passphrase, /kem/salt, s, t, p),
+    info="keks/cm/encrypted/balloon-blake2b-hkdf" || /id)
+@end verbatim
+
+@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism.
diff --git a/spec/cm/kem-gost3410-hkdf.texi b/spec/cm/kem-gost3410-hkdf.texi
new file mode 100644 (file)
index 0000000..adba835
--- /dev/null
@@ -0,0 +1,27 @@
+@node kem-gost3410-hkdf
+@cindex kem-gost3410-hkdf
+@nodedescription GOST R 34.10+HKDF KEM
+@subsubsection GOST R 34.10+HKDF KEM
+
+@code{/kem/*/a} equals to "gost3410-hkdf".
+Recipient map must also contain additional fields:
+
+@table @code
+@item /to/*/ukm: bytes
+    Additional 16-bytes keying material.
+@item /to/*/pub: bytes
+    Sender's ephemeral 512-bit public key.
+@end table
+
+ГОСТ Р 34.10-2012 VKO parameter set A/C ("gost3410-256A", "gost3410-512C")
+must be used for DH operation, with UKM taken from the structure. VKO's
+output is 512- or 1024-bit @code{BE(X)||BE(Y)} point. It is used in HKDF
+and KExp15 (Р 1323565.1.017) key wrapping algorithm:
+
+@verbatim
+PRK = HKDF-Extract(Streebog-512, salt="", ikm=VKO(..., ukm=UKM))
+KEK= HKDF-Expand(Streebog-512, prk=PRK,
+    info="keks/cm/encrypted/gost3410-hkdf" || /id)
+@end verbatim
+
+@code{/kem/*/cek} is wrapped with @ref{keywrap-kexp15} mechanism.
diff --git a/spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi b/spec/cm/kem-mceliece6960119-x25519-hkdf-shake256.texi
new file mode 100644 (file)
index 0000000..9c00499
--- /dev/null
@@ -0,0 +1,32 @@
+@node kem-mceliece6960119-x25519-hkdf-shake256
+@cindex kem-mceliece6960119-x25519-hkdf-shake256
+@nodedescription Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM
+@subsubsection Classic McEliece 6960-119+X25519+HKDF-SHAKE256 KEM
+
+@code{/kem/*/a} equals to "mceliece6960119-x25519-hkdf-shake256".
+Recipient public key with
+@ref{cm-pub-mceliece6960119-x25519, @code{mceliece6960119-x25519}}
+algorithm must be used. It should have "kem" key usage set.
+
+Recipient map must also contain additional field:
+@code{/kem/*/encap: bytes} -- concatenation of 194 bytes of
+Classic McEliece 6960-119 ciphertext with 32 bytes of ephemeral
+X25519 public key.
+
+Recipient performs X25519 and Classic McEliece computations to
+derive/decapsulate two 32-byte shared keys. Then it combines
+them to get the KEK decryption key of the CEK.
+
+@verbatim
+PRK = HKDF-Extract(SHAKE256, salt="", ikm=
+        mceliece6960119-sender-ciphertext ||
+        x25519-sender-public-key ||
+        mceliece6960119-recipient-public-key ||
+        x25519-recipient-public-key ||
+        mceliece6960119-shared-key ||
+        x25519-shared-key)[:32]
+KEK = HKDF-Expand(SHAKE256, prk=PRK,
+    info="keks/cm/encrypted/mceliece6960119-x25519-hkdf-shake256" || /salt)
+@end verbatim
+
+@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism.
diff --git a/spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi b/spec/cm/kem-sntrup4591761-x25519-hkdf-blake2b.texi
new file mode 100644 (file)
index 0000000..fca71c7
--- /dev/null
@@ -0,0 +1,31 @@
+@node kem-sntrup4591761-x25519-hkdf-blake2b
+@cindex kem-sntrup4591761-x25519-hkdf-blake2b
+@nodedescription SNTRUP4591761+X25519+HKDF-BLAKE2b KEM
+@subsubsection SNTRUP4591761+X25519+HKDF-BLAKE2b KEM
+
+@code{/kem/*/a} equals to "sntrup4591761-x25519-hkdf-blake2b".
+Recipient public key with @ref{cm-pub-sntrup4591761-x25519,
+@code{sntrup4591761-x25519}} algorithm must be used. It should have
+"kem" key usage set.
+
+Recipient map must also contain additional field: @code{/kem/*/encap:
+bytes} -- concatenation of 1047 bytes of Streamlined NTRU Prime
+4591^761's ciphertext with 32 bytes of ephemeral X25519 public key.
+
+Recipient performs X25519 and SNTRUP computations to derive/decapsulate
+two 32-byte shared keys. Then it combines them to get the KEK decryption
+key of the CEK.
+
+@verbatim
+PRK = HKDF-Extract(BLAKE2b, salt="", ikm=
+        sntrup4591761-sender-ciphertext ||
+        x25519-sender-public-key ||
+        sntrup4591761-recipient-public-key ||
+        x25519-recipient-public-key ||
+        sntrup4591761-shared-key ||
+        x25519-shared-key)
+KEK = HKDF-Expand(BLAKE2b, prk=PRK,
+    info="keks/cm/encrypted/sntrup4591761-x25519-hkdf-blake2b" || /id)
+@end verbatim
+
+@code{/kem/*/cek} is wrapped with @ref{keywrap-xchapoly} mechanism.
diff --git a/spec/cm/keywrap-kexp15.texi b/spec/cm/keywrap-kexp15.texi
new file mode 100644 (file)
index 0000000..d68149b
--- /dev/null
@@ -0,0 +1,13 @@
+@node keywrap-kexp15
+@cindex keywrap-kexp15
+@nodedescription KExp15 key wrapping mechanism
+@subsubsection KExp15 key wrapping mechanism
+
+KExp15 (Р 1323565.1.017) key wrapping mechanism uses GOST (ГОСТ)
+cryptography algorithms. KEK is 32+8+32=72 bytes long.
+
+@verbatim
+Kenc || IV || Kauth = KEK
+KExp15(Kenc, Kauth, IV, CEK) = Kuznechik-CTR(
+    Kenc, CEK || Kuznechik-CMAC(Kauth, IV || CEK), IV=IV)
+@end verbatim
diff --git a/spec/cm/keywrap-xchapoly.texi b/spec/cm/keywrap-xchapoly.texi
new file mode 100644 (file)
index 0000000..331740d
--- /dev/null
@@ -0,0 +1,13 @@
+@node keywrap-xchapoly
+@cindex keywrap-xchapoly
+@nodedescription XChaCha20-Poly1305 key wrapping mechanism
+@subsubsection XChaCha20-Poly1305 key wrapping mechanism
+
+Key is encrypted using XChaCha20-Poly1305 algorithm.
+Random 192-bit nonce is prepended to the ciphertext.
+KEK has 256-bit length.
+
+@verbatim
+NONCE = random(24 bytes)
+NONCE || XChaCha20-Poly1305(key=KEK, ad="", nonce=NONCE, data=CEK)
+@end verbatim