"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"
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"
return
}
}
- if encrypted.DEM.A != cmenc.ChaCha20Poly1305 {
+ if encrypted.DEM.A != chapoly.DEMAlgo {
err = errors.New("unsupported prv encryption DEM")
return
}
}
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
}
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")
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")
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 {
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
}
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
}
}
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
}
}
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:")
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),
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)
}
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]
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]
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()
}
}
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)
}
}
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"
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"
package balloon
import (
- "bytes"
"crypto/hkdf"
"errors"
"hash"
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 (
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")
}
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)
}
// 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 {
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{}{}
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
}
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)
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
}
--- /dev/null
+// 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,
+ )
+}
package encrypted
-const ChaCha20Poly1305 = "chacha20poly1305"
-
type DEM struct {
A string `keks:"a"`
}
DEM DEM `keks:"dem"`
KEM []KEM `keks:"kem"`
Payload []byte `keks:"payload,omitempty"`
- Salt uuid.UUID `keks:"salt"`
+ Id uuid.UUID `keks:"id"`
}
--- /dev/null
+@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.
--- /dev/null
+@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.
-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,
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
--- /dev/null
+@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.
--- /dev/null
+@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.
--- /dev/null
+@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.
--- /dev/null
+@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.
--- /dev/null
+@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
--- /dev/null
+@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