From: Sergey Matveev Date: Sun, 16 Feb 2025 07:02:58 +0000 (+0300) Subject: Various refactoring and passphrase-encrypted private keys support X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=581165b796992ccfc49c16d4f71102edc37ad73220ee4aad73d28cdc202cc53a;p=keks.git Various refactoring and passphrase-encrypted private keys support --- diff --git a/c/cmd/pp/pp.c b/c/cmd/pp/pp.c index 3b5f253..ea68877 100644 --- a/c/cmd/pp/pp.c +++ b/c/cmd/pp/pp.c @@ -44,7 +44,7 @@ static char *ColourCyan = "\x1b[36m"; static char *ColourWhite = "\x1b[37m"; static char *ColourReset = "\x1b[0m"; -static size_t MaxStrLen = 40; +static size_t MaxStrLen = 64; static bool NoOffsets = false; static int OffDigits = 0; static char OffFmt[16] = {0}; diff --git a/go/.gitignore b/go/.gitignore new file mode 100644 index 0000000..0d4fc74 --- /dev/null +++ b/go/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/go/cm/.gitignore b/go/cm/.gitignore new file mode 100644 index 0000000..0d4fc74 --- /dev/null +++ b/go/cm/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/go/cm/cmd/enctool/main.go b/go/cm/cmd/enctool/main.go index 3c1aaa2..4910826 100644 --- a/go/cm/cmd/enctool/main.go +++ b/go/cm/cmd/enctool/main.go @@ -28,6 +28,7 @@ import ( "log" "os" "runtime" + "strconv" "github.com/companyzero/sntrup4591761" "github.com/google/uuid" @@ -38,6 +39,8 @@ import ( "go.cypherpunks.su/keks" "go.cypherpunks.su/keks/cm" 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" mceliece6960119x25519 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519" mceliece6960119 "go.cypherpunks.su/keks/cm/enc/mceliece6960119-x25519/mceliece6960119" @@ -87,17 +90,68 @@ func readPasswd(prompt string) (passwd []byte) { return } +func readPrv(pth string) (*cm.AV, error) { + data := mustReadFile(pth) + var magic keks.Magic + magic, data = keks.StripMagic(data) + switch magic { + case sign.PrvMagic: + case cmenc.Magic: + var encrypted cmenc.Encrypted + { + d := keks.NewDecoderFromBytes(data, nil) + if err := d.DecodeStruct(&encrypted); err != nil { + return nil, err + } + } + if encrypted.DEM.A != cmenc.ChaCha20Poly1305 { + return nil, errors.New("unsupported prv encryption DEM") + } + if len(encrypted.KEM) != 1 || + encrypted.KEM[0].A != cmballoon.BalloonBLAKE2bHKDF || + len(encrypted.Payload) == 0 { + return nil, errors.New("wrong prv encryption KEM") + } + passwd := readPasswd("Passphrase for " + pth + ":") + cek, err := cmballoon.Decapsulate( + encrypted.KEM[0], + encrypted.Salt[:], + passwd, + ) + if err != nil { + return nil, err + } + var buf bytes.Buffer + _, err = chaPoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1) + if err != nil { + return nil, err + } + data = buf.Bytes() + magic, data = keks.StripMagic(data) + if magic == sign.PrvMagic { + break + } + fallthrough + default: + return nil, errors.New("wrong magic") + } + var av cm.AV + d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) + return &av, d.DecodeStruct(&av) +} + func main() { log.SetFlags(log.Lshortfile) flag.Usage = usage setSalt := flag.String("salt", "", "Set that /salt 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<<16, "Balloon's space cost") - balloonT := flag.Int("balloon-t", 3, "Balloon's time cost") + balloonS := flag.Int("balloon-s", 1<<17, "Balloon's space cost") + balloonT := flag.Int("balloon-t", 4, "Balloon's time cost") balloonP := flag.Int("balloon-p", 2, "Balloon's number of threads") doDecrypt := flag.Bool("d", false, "Decrypt") parallel := flag.Int("parallel", runtime.NumCPU(), "Parallel cryptors") + noblob := flag.Bool("no-stream", false, "Include payload into container") var pubs []*sign.Pub flag.Func("pub", "Path to public key to encrypt to", func(v string) error { signed, err := sign.PubParse(mustReadFile(v)) @@ -119,18 +173,12 @@ func main() { return err }) var prvs []*cm.AV - flag.Func("prv", "Our private keys for decryption", func(v string) (err error) { - magic, data := keks.StripMagic(mustReadFile(v)) - if magic == "" || magic != sign.PrvMagic { - return errors.New("wrong magic") + flag.Func("prv", "Our private keys for decryption", func(v string) error { + av, err := readPrv(v) + if err == nil { + prvs = append(prvs, av) } - var av cm.AV - d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16}) - if err = d.DecodeStruct(&av); err != nil { - return err - } - prvs = append(prvs, &av) - return nil + return err }) flag.Parse() @@ -167,45 +215,24 @@ func main() { } for kemIdx, kem := range encrypted.KEM { switch kem.A { - case cmenc.BalloonBLAKE2bHKDF: + case cmballoon.BalloonBLAKE2bHKDF: if !*passphrase { log.Println(kemIdx, kem.A, "skipping because no -passwd") continue } - if kem.Salt == nil { - log.Fatalln("missing salt") - } - if kem.Cost == nil { - log.Fatalln("missing cost") + passwd := readPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:") + cek, err = cmballoon.Decapsulate( + kem, + encrypted.Salt[:], + passwd, + ) + if err != nil { + log.Print(err) + continue } - passwd := readPasswd("Passphrase:") - { - var kek []byte - kek, err = hkdf.Expand( - blake2bHash, - balloon.H( - blake2bHash, - passwd, - append(encrypted.Salt[:], *kem.Salt...), - int(kem.Cost.S), int(kem.Cost.T), int(kem.Cost.P), - ), - cmenc.BalloonHKDFInfo, - chaPoly.KeyLen, - ) - if err != nil { - log.Fatal(err) - } - var cekp bytes.Buffer - _, err = chaPoly.Open(&cekp, bytes.NewReader(kem.CEK), kek, 1) - if err != nil { - log.Println(kemIdx, kem.A, err, ", skipping") - continue - } - if cekp.Len() != chaPoly.KeyLen { - log.Println(kemIdx, kem.A, "wrong key len, skipping") - continue - } - cek = cekp.Bytes() + if len(cek) != chaPoly.KeyLen { + log.Println(kemIdx, kem.A, "wrong key len, skipping") + continue } case sntrup4591761x25519.SNTRUP4591761X25519HKDFBLAKE2b: if len(prvs) == 0 { @@ -397,7 +424,11 @@ func main() { if cek == nil { log.Fatal("no KEMs processed") } - _, err = chaPoly.Open(os.Stdout, os.Stdin, cek, *parallel) + if len(encrypted.Payload) > 0 { + _, err = chaPoly.Open(os.Stdout, bytes.NewReader(encrypted.Payload), cek, *parallel) + } else { + _, err = chaPoly.OpenBlob(os.Stdout, os.Stdin, cek, *parallel) + } if err != nil { log.Fatal(err) } @@ -422,12 +453,12 @@ func main() { log.Fatal("passphrases do not match") } } - bSalt := make([]byte, cmenc.BalloonSaltLen) + bSalt := make([]byte, cmballoon.SaltLen) rand.Read(bSalt) kem := cmenc.KEM{ - A: cmenc.BalloonBLAKE2bHKDF, + A: cmballoon.BalloonBLAKE2bHKDF, Salt: &bSalt, - Cost: &cmenc.BalloonCost{ + BalloonCost: &ballooncost.Cost{ S: uint64(*balloonS), T: uint64(*balloonT), P: uint64(*balloonP), @@ -442,7 +473,7 @@ func main() { append(salt[:], bSalt...), *balloonS, *balloonT, *balloonP, ), - cmenc.BalloonHKDFInfo, + cmballoon.HKDFInfo, chaPoly.KeyLen, ) if err != nil { @@ -606,19 +637,29 @@ func main() { if _, err = keks.Encode(&hdr, cmenc.Magic, nil); err != nil { log.Fatal(err) } - if _, err = keks.Encode(&hdr, &cmenc.Encrypted{ + enc := cmenc.Encrypted{ Salt: salt, KEM: kems, DEM: cmenc.DEM{A: cmenc.ChaCha20Poly1305}, - }, nil); err != nil { + } + if *noblob { + var buf bytes.Buffer + if _, err = chaPoly.Seal(&buf, os.Stdin, cek, *parallel); err != nil { + log.Fatal(err) + } + enc.Payload = buf.Bytes() + } + if _, err = keks.Encode(&hdr, &enc, nil); err != nil { log.Fatal(err) } if _, err = io.Copy(os.Stdout, &hdr); err != nil { log.Fatal(err) } } - if _, err = chaPoly.Seal(os.Stdout, os.Stdin, cek, *parallel); err != nil { - log.Fatal(err) + if !*noblob { + if _, err = chaPoly.SealBlob(os.Stdout, os.Stdin, cek, *parallel); err != nil { + log.Fatal(err) + } } } } diff --git a/go/cm/cmd/enctool/passphrase.t b/go/cm/cmd/enctool/passphrase.t index a1e40c2..66506de 100755 --- a/go/cm/cmd/enctool/passphrase.t +++ b/go/cm/cmd/enctool/passphrase.t @@ -1,15 +1,15 @@ #!/bin/sh -test_description="Check that basic passphrase encryption functionality works" +test_description="Check passphrase encryption" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} dd if=/dev/urandom of=$TMPDIR/enc.data bs=300K count=1 2>/dev/null export ENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) -test_expect_success "encrypting" "enctool -p \ +test_expect_success "encrypting" "cmenctool -p \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" -test_expect_success "decrypting" "enctool -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" diff --git a/go/cm/cmd/enctool/prv-encrypted.t b/go/cm/cmd/enctool/prv-encrypted.t new file mode 100755 index 0000000..dc9ba92 --- /dev/null +++ b/go/cm/cmd/enctool/prv-encrypted.t @@ -0,0 +1,21 @@ +#!/bin/sh + +test_description="Check passphrase-encrypted key decryption" +. $SHARNESS_TEST_SRCDIR/sharness.sh + +TMPDIR=${TMPDIR:-/tmp} + +cmkeytool -algo sntrup4591761-x25519 -ku kem -subj A=KEY \ + -prv $TMPDIR/enc.prv -pub $TMPDIR/enc.pub +dd if=/dev/urandom of=$TMPDIR/enc.data bs=12K count=1 2>/dev/null +export ENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) +test_expect_success "key encrypting" "cmenctool -p -no-blob \ + <$TMPDIR/enc.prv >$TMPDIR/enc.prv.enc" +test_expect_success "data encrypting" "cmenctool -pub $TMPDIR/enc.pub \ + <$TMPDIR/enc.data >$TMPDIR/enc.enc" +test_expect_success "decrypting" "cmenctool -d -prv $TMPDIR/enc.prv.enc \ + <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" +test_expect_success "comparing" \ + "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" + +test_done diff --git a/go/cm/cmd/enctool/pub.t b/go/cm/cmd/enctool/pub.t index ec464d6..5d7065d 100755 --- a/go/cm/cmd/enctool/pub.t +++ b/go/cm/cmd/enctool/pub.t @@ -1,6 +1,6 @@ #!/bin/sh -test_description="Check that basic public-key encryption functionality works" +test_description="Check public-key encryption" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} @@ -9,47 +9,47 @@ dd if=/dev/urandom of=$TMPDIR/enc.data bs=300K count=1 2>/dev/null algo=mceliece6960119-x25519 algo0=$algo -test_expect_success "$algo: pub generation" "keytool \ +test_expect_success "$algo: pub generation" "cmkeytool \ -algo $algo -ku kem -subj A=$algo \ -prv $TMPDIR/enc.$algo.prv -pub $TMPDIR/enc.$algo.pub" algo=sntrup4591761-x25519 algo1=$algo -test_expect_success "$algo: pub generation" "keytool \ +test_expect_success "$algo: pub generation" "cmkeytool \ -algo $algo -ku kem -subj A=$algo \ -prv $TMPDIR/enc.$algo.prv -pub $TMPDIR/enc.$algo.pub" -test_expect_success "encrypting" "enctool \ +test_expect_success "encrypting" "cmenctool \ -pub $TMPDIR/enc.$algo0.pub -pub $TMPDIR/enc.$algo1.pub \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" -test_expect_success "any: decrypting" "enctool -d \ +test_expect_success "any: decrypting" "cmenctool -d \ -prv $TMPDIR/enc.$algo0.prv -prv $TMPDIR/enc.$algo1.prv \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" -test_expect_success "$algo0: decrypting" "enctool -d \ +test_expect_success "$algo0: decrypting" "cmenctool -d \ -prv $TMPDIR/enc.$algo0.prv \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "$algo0: comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" -test_expect_success "$algo1: decrypting" "enctool -d \ +test_expect_success "$algo1: decrypting" "cmenctool -d \ -prv $TMPDIR/enc.$algo1.prv \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "$algo1: comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" export ENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p) -test_expect_success "encrypting also with passphrase" "enctool \ +test_expect_success "encrypting also with passphrase" "cmenctool \ -pub $TMPDIR/enc.$algo0.pub -pub $TMPDIR/enc.$algo1.pub -p \ <$TMPDIR/enc.data >$TMPDIR/enc.enc" -test_expect_success "any: decrypting" "enctool -d \ +test_expect_success "any: decrypting" "cmenctool -d \ -prv $TMPDIR/enc.$algo0.prv -prv $TMPDIR/enc.$algo1.prv \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" -test_expect_success "passphrase: decrypting" "enctool -d -p \ +test_expect_success "passphrase: decrypting" "cmenctool -d -p \ <$TMPDIR/enc.enc >$TMPDIR/enc.data.got" test_expect_success "comparing" \ "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got" diff --git a/go/cm/cmd/hshtool/main.go b/go/cm/cmd/hshtool/main.go index 6c2b9e3..d43cc4b 100644 --- a/go/cm/cmd/hshtool/main.go +++ b/go/cm/cmd/hshtool/main.go @@ -2,7 +2,6 @@ package main import ( "crypto/sha512" - "encoding/hex" "flag" "fmt" "log" @@ -11,6 +10,8 @@ import ( "go.cypherpunks.su/gogost/v6/gost34112012256" "go.cypherpunks.su/gogost/v6/gost34112012512" + + "go.cypherpunks.su/keks" cmhash "go.cypherpunks.su/keks/cm/hash" cmblake2b "go.cypherpunks.su/keks/cm/hash/blake2b" "go.cypherpunks.su/keks/cm/hash/gost" @@ -20,7 +21,7 @@ import ( func main() { workers := flag.Int("p", runtime.NumCPU(), "Parallel workers") chunkLenK := flag.Int("c", merkle.DefaultChunkLen/1024, "Chunk size, KiB") - algo := flag.String("a", cmhash.BLAKE2b, "Algorithm to use") + algo := flag.String("a", cmhash.BLAKE2bMerkle, "Algorithm to use") list := flag.Bool("list", false, "List available algorithms") mmap := flag.String("mmap", "", "Use that mmap-ed file instead of stdin") flag.Parse() @@ -60,5 +61,10 @@ func main() { if err != nil { log.Fatal(err) } - fmt.Println(hex.EncodeToString(hasher.Sum(nil))) + s := cmhash.Hashed{ + Algo: []string{*algo}, + Typ: "data", + Hash: [][]byte{hasher.Sum(nil)}, + } + keks.Encode(os.Stdout, s, nil) } diff --git a/go/cm/cmd/keytool/certification.t b/go/cm/cmd/keytool/certification.t index f2780bb..30f68e9 100755 --- a/go/cm/cmd/keytool/certification.t +++ b/go/cm/cmd/keytool/certification.t @@ -1,6 +1,6 @@ #!/bin/sh -test_description="Check that basic certification functionality works" +test_description="Check certification" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} @@ -9,42 +9,42 @@ echo "gost3410-512C gost3410-256A ed25519-blake2b ed25519-blake2b" | while read caAlgo eeAlgo ; do subj="-subj CN=CA -subj C=RU" -test_expect_success "$caAlgo: CA load generation" "keytool \ +test_expect_success "$caAlgo: CA load generation" "cmkeytool \ -algo $caAlgo \ -ku sig $subj \ -prv $TMPDIR/ca.$caAlgo.prv -pub $TMPDIR/ca.$caAlgo.pub" -test_expect_success "$caAlgo: CA generation" "keytool \ +test_expect_success "$caAlgo: CA generation" "cmkeytool \ -pub $TMPDIR/ca.$caAlgo.pub \ -ca-prv $TMPDIR/ca.$caAlgo.prv -ca-pub $TMPDIR/ca.$caAlgo.pub" -test_expect_success "$caAlgo: CA regeneration" "keytool \ +test_expect_success "$caAlgo: CA regeneration" "cmkeytool \ -pub $TMPDIR/ca.$caAlgo.pub \ -ca-prv $TMPDIR/ca.$caAlgo.prv -ca-pub $TMPDIR/ca.$caAlgo.pub" -test_expect_success "$caAlgo: CA self-signature" "keytool \ +test_expect_success "$caAlgo: CA self-signature" "cmkeytool \ -ca-pub $TMPDIR/ca.$caAlgo.pub \ -pub $TMPDIR/ca.$caAlgo.pub \ -verify" subj="-subj CN=SubCA -subj C=RU" -test_expect_success "$eeAlgo: SubCA load generation" "keytool \ +test_expect_success "$eeAlgo: SubCA load generation" "cmkeytool \ -algo $eeAlgo \ -ku sig $subj \ -prv $TMPDIR/subca.$eeAlgo.prv -pub $TMPDIR/subca.$eeAlgo.pub" -test_expect_success "$eeAlgo: SubCA generation" "keytool \ +test_expect_success "$eeAlgo: SubCA generation" "cmkeytool \ -pub $TMPDIR/subca.$eeAlgo.pub \ -ca-pub $TMPDIR/ca.$caAlgo.pub -ca-prv $TMPDIR/ca.$caAlgo.prv" -test_expect_success "$eeAlgo: SubCA signature" "keytool \ +test_expect_success "$eeAlgo: SubCA signature" "cmkeytool \ -ca-pub $TMPDIR/ca.$caAlgo.pub \ -pub $TMPDIR/subca.$eeAlgo.pub \ -verify" subj="-subj CN=EE -subj C=RU" -test_expect_success "$eeAlgo: EE load generation" "keytool \ +test_expect_success "$eeAlgo: EE load generation" "cmkeytool \ -algo $eeAlgo $subj \ -prv $TMPDIR/ee.$eeAlgo.prv -pub $TMPDIR/ee.$eeAlgo.pub" -test_expect_success "$eeAlgo: EE generation" "keytool \ +test_expect_success "$eeAlgo: EE generation" "cmkeytool \ -ca-prv $TMPDIR/subca.$eeAlgo.prv -ca-pub $TMPDIR/subca.$eeAlgo.pub \ -pub $TMPDIR/ee.$eeAlgo.pub" -test_expect_success "$eeAlgo: EE chain" "keytool \ +test_expect_success "$eeAlgo: EE chain" "cmkeytool \ -ca-pub $TMPDIR/ca.$caAlgo.pub \ -ca-pub $TMPDIR/subca.$eeAlgo.pub \ -pub $TMPDIR/ee.$eeAlgo.pub \ diff --git a/go/cm/cmd/keytool/kem-generation.t b/go/cm/cmd/keytool/kem-generation.t index 6cb90c9..0b18e9c 100755 --- a/go/cm/cmd/keytool/kem-generation.t +++ b/go/cm/cmd/keytool/kem-generation.t @@ -1,6 +1,6 @@ #!/bin/sh -test_description="Check that KEM certificates generation works" +test_description="Check KEM certificates generation" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} @@ -8,7 +8,7 @@ TMPDIR=${TMPDIR:-/tmp} echo "mceliece6960119-x25519 sntrup4591761-x25519" | while read algo ; do -test_expect_success "$algo: generation" "keytool \ +test_expect_success "$algo: generation" "cmkeytool \ -algo $algo \ -ku kem -subj CN=DH \ -prv $TMPDIR/kem.$algo.prv -pub $TMPDIR/kem.$algo.pub" diff --git a/go/cm/cmd/sigtool/basic.t b/go/cm/cmd/sigtool/basic.t index 3a1e12d..14d6111 100755 --- a/go/cm/cmd/sigtool/basic.t +++ b/go/cm/cmd/sigtool/basic.t @@ -1,6 +1,6 @@ #!/bin/sh -test_description="Check that basic signing functionality works" +test_description="Check signing" . $SHARNESS_TEST_SRCDIR/sharness.sh TMPDIR=${TMPDIR:-/tmp} @@ -11,7 +11,7 @@ ed25519-blake2b" | while read keyalgo ; do subj="-subj what=ever" typ="some-different-type" -test_expect_success "$keyalgo: pub generation" "keytool \ +test_expect_success "$keyalgo: pub generation" "cmkeytool \ -algo $keyalgo -ku sig $subj \ -prv $TMPDIR/sign.$keyalgo.prv -pub $TMPDIR/sign.$keyalgo.pub" dd if=/dev/urandom of=$TMPDIR/sign.$keyalgo.data bs=300K count=1 2>/dev/null @@ -21,32 +21,32 @@ badEncTo="-encrypted-to $(uuidgen)" for merkle in "" "-merkle" ; do algo=${keyalgo}${merkle} -test_expect_success "$algo: signing" "sigtool $merkle \ +test_expect_success "$algo: signing" "cmsigtool $merkle \ -prv $TMPDIR/sign.$keyalgo.prv -pub $TMPDIR/sign.$keyalgo.pub -type $typ \ $encTo <$TMPDIR/sign.$keyalgo.data >$TMPDIR/sign.$algo.sig" -test_expect_success "$algo: verifying" "sigtool \ +test_expect_success "$algo: verifying" "cmsigtool \ -verify -pub $TMPDIR/sign.$keyalgo.pub -type $typ \ <$TMPDIR/sign.$algo.sig >$TMPDIR/sign.data.got" test_expect_success "$algo: comparing" \ "test_cmp $TMPDIR/sign.$keyalgo.data $TMPDIR/sign.data.got" -test_expect_success "$algo: differing type" "! sigtool \ +test_expect_success "$algo: differing type" "! cmsigtool \ -verify -pub $TMPDIR/sign.$keyalgo.pub <$TMPDIR/sign.$algo.sig >/dev/null" -test_expect_success "$algo: good encTo" "! sigtool \ +test_expect_success "$algo: good encTo" "! cmsigtool \ -verify -pub $TMPDIR/sign.$keyalgo.pub $encTo <$TMPDIR/sign.$algo.sig >/dev/null" -test_expect_success "$algo: bad encTo" "! sigtool \ +test_expect_success "$algo: bad encTo" "! cmsigtool \ -verify -pub $TMPDIR/sign.$keyalgo.pub $badEncTo <$TMPDIR/sign.$algo.sig >/dev/null" -test_expect_success "$algo: detached signing" "sigtool -detached $merkle \ +test_expect_success "$algo: detached signing" "cmsigtool -detached $merkle \ -prv $TMPDIR/sign.$keyalgo.prv -pub $TMPDIR/sign.$keyalgo.pub -type $typ \ <$TMPDIR/sign.$keyalgo.data >$TMPDIR/sign.$algo.detached.sig" test_expect_success "$algo: detached verifying" \ "cat $TMPDIR/sign.$algo.detached.sig $TMPDIR/sign.$keyalgo.data | - sigtool -detached -verify -pub $TMPDIR/sign.$keyalgo.pub -type $typ" -test_expect_success "$algo: differing type" "! sigtool -detached \ + cmsigtool -detached -verify -pub $TMPDIR/sign.$keyalgo.pub -type $typ" +test_expect_success "$algo: differing type" "! cmsigtool -detached \ -verify -pub $TMPDIR/sign.$keyalgo.pub <$TMPDIR/sign.$algo.detached.sig >/dev/null" -test_expect_success "$algo: good encTo" "! sigtool -detached \ +test_expect_success "$algo: good encTo" "! cmsigtool -detached \ -verify -pub $TMPDIR/sign.$keyalgo.pub $encTo <$TMPDIR/sign.$algo.detached.sig >/dev/null" -test_expect_success "$algo: bad encTo" "! sigtool -detached \ +test_expect_success "$algo: bad encTo" "! cmsigtool -detached \ -verify -pub $TMPDIR/sign.$keyalgo.pub $badEncTo <$TMPDIR/sign.$algo.detached.sig >/dev/null" done diff --git a/go/cm/enc/balloon.go b/go/cm/enc/balloon.go deleted file mode 100644 index 87cc069..0000000 --- a/go/cm/enc/balloon.go +++ /dev/null @@ -1,12 +0,0 @@ -package encrypted - -const ( - BalloonSaltLen = 8 - BalloonHKDFInfo = "keks/cm/encrypted/balloon-blake2b-hkdf" -) - -type BalloonCost struct { - S uint64 `keks:"s"` - T uint64 `keks:"t"` - P uint64 `keks:"p"` -} diff --git a/go/cm/enc/balloon/cost/cost.go b/go/cm/enc/balloon/cost/cost.go new file mode 100644 index 0000000..b723b60 --- /dev/null +++ b/go/cm/enc/balloon/cost/cost.go @@ -0,0 +1,7 @@ +package cost + +type Cost struct { + S uint64 `keks:"s"` + T uint64 `keks:"t"` + P uint64 `keks:"p"` +} diff --git a/go/cm/enc/balloon/decap.go b/go/cm/enc/balloon/decap.go new file mode 100644 index 0000000..32a16da --- /dev/null +++ b/go/cm/enc/balloon/decap.go @@ -0,0 +1,71 @@ +// GoKEKS/CM -- KEKS-encoded cryptographic messages +// Copyright (C) 2024-2025 Sergey Matveev +// +// 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 . + +package balloon + +import ( + "bytes" + "crypto/hkdf" + "errors" + "hash" + + "go.cypherpunks.su/balloon/v3" + cmenc "go.cypherpunks.su/keks/cm/enc" + chaPoly "go.cypherpunks.su/keks/cm/enc/chapoly" + "golang.org/x/crypto/blake2b" +) + +const ( + BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf" + SaltLen = 8 + HKDFInfo = "keks/cm/encrypted/balloon-blake2b-hkdf" +) + +func blake2bHash() hash.Hash { + h, err := blake2b.New512(nil) + if err != nil { + panic(err) + } + return h +} + +func Decapsulate(kem cmenc.KEM, encSalt, passphrase []byte) (cek []byte, err error) { + if kem.Salt == nil { + return nil, errors.New("missing salt") + } + if kem.BalloonCost == nil { + return nil, errors.New("missing cost") + } + var kek []byte + kek, err = hkdf.Expand( + blake2bHash, + balloon.H( + blake2bHash, + passphrase, + append(encSalt, *kem.Salt...), + int(kem.BalloonCost.S), + int(kem.BalloonCost.T), + int(kem.BalloonCost.P), + ), + HKDFInfo, + chaPoly.KeyLen, + ) + if err != nil { + return nil, err + } + var buf bytes.Buffer + _, err = chaPoly.Open(&buf, bytes.NewReader(kem.CEK), kek, 1) + return buf.Bytes(), err +} diff --git a/go/cm/enc/chapoly/dem.go b/go/cm/enc/chapoly/dem.go index 021a2b6..d9d2700 100644 --- a/go/cm/enc/chapoly/dem.go +++ b/go/cm/enc/chapoly/dem.go @@ -50,7 +50,7 @@ type job struct { func do( w io.Writer, - seal bool, + blob, seal bool, r io.Reader, key []byte, procs int, @@ -76,9 +76,13 @@ func do( blobChunkLen := PadLen + ChunkLen + overhead var blobDecoder *keks.BlobDecoder if seal { - total, err = keks.BlobAtomEncode(w, int64(blobChunkLen)) + if blob { + total, err = keks.BlobAtomEncode(w, int64(blobChunkLen)) + } } else { - blobDecoder, err = keks.NewBlobDecoder(r, int64(blobChunkLen)) + if blob { + blobDecoder, err = keks.NewBlobDecoder(r, int64(blobChunkLen)) + } } if err != nil { return @@ -126,7 +130,11 @@ func do( for j := range dones { <-j.processed if seal { - n, errW = keks.BinEncode(w, j.buf) + if blob { + n, errW = keks.BinEncode(w, j.buf) + } else { + n, errW = io.Copy(w, bytes.NewReader(j.buf)) + } } else { if len(j.buf) == 0 { n = 0 @@ -135,7 +143,7 @@ func do( } } total += n - if errW != nil { + if errW != nil || n == 0 { break } j.buf = j.buf[:blobChunkLen] @@ -159,10 +167,15 @@ func do( if seal { n, errR = io.ReadFull(r, j.buf[PadLen:PadLen+ChunkLen]) } else { - blobDecoder.SetReadBuf(j.buf) - chunk, errR = blobDecoder.Next() - n = len(chunk) - j.buf = j.buf[:n] + if blobDecoder == nil { + n, errR = io.ReadFull(r, j.buf) + j.buf = j.buf[:n] + } else { + blobDecoder.SetReadBuf(j.buf) + chunk, errR = blobDecoder.Next() + n = len(chunk) + j.buf = j.buf[:n] + } } total += int64(n) if errR != nil { @@ -202,9 +215,17 @@ func do( } func Seal(w io.Writer, r io.Reader, key []byte, procs int) (total int64, err error) { - return do(w, true, r, key, procs) + return do(w, false, true, r, key, procs) } func Open(w io.Writer, r io.Reader, key []byte, procs int) (total int64, err error) { - return do(w, false, r, key, procs) + return do(w, false, false, r, key, procs) +} + +func SealBlob(w io.Writer, r io.Reader, key []byte, procs int) (total int64, err error) { + return do(w, true, true, r, key, procs) +} + +func OpenBlob(w io.Writer, r io.Reader, key []byte, procs int) (total int64, err error) { + return do(w, true, false, r, key, procs) } diff --git a/go/cm/enc/dem.go b/go/cm/enc/dem.go index 0165666..0f289b2 100644 --- a/go/cm/enc/dem.go +++ b/go/cm/enc/dem.go @@ -1,6 +1,7 @@ package encrypted -const ( - BalloonBLAKE2bHKDF = "balloon-blake2b-hkdf" - ChaCha20Poly1305 = "chacha20poly1305" -) +const ChaCha20Poly1305 = "chacha20poly1305" + +type DEM struct { + A string `keks:"a"` +} diff --git a/go/cm/enc/enc.go b/go/cm/enc/enc.go new file mode 100644 index 0000000..2a84f5c --- /dev/null +++ b/go/cm/enc/enc.go @@ -0,0 +1,11 @@ +package encrypted + +import "github.com/google/uuid" + +type Encrypted struct { + DEM DEM `keks:"dem"` + KEM []KEM `keks:"kem"` + Salt uuid.UUID `keks:"salt"` + + Payload []byte `keks:"payload,omitempty"` +} diff --git a/go/cm/enc/kem.go b/go/cm/enc/kem.go index 497f4f6..94a7a95 100644 --- a/go/cm/enc/kem.go +++ b/go/cm/enc/kem.go @@ -2,7 +2,8 @@ package encrypted import ( "github.com/google/uuid" - "go.cypherpunks.su/keks" + + balloon "go.cypherpunks.su/keks/cm/enc/balloon/cost" ) const ( @@ -16,20 +17,8 @@ type KEM struct { To *uuid.UUID `keks:"to,omitempty"` // balloon-blake2b-hkdf related - Cost *BalloonCost `keks:"cost,omitempty"` - Salt *[]byte `keks:"salt,omitempty"` + BalloonCost *balloon.Cost `keks:"cost,omitempty"` + Salt *[]byte `keks:"salt,omitempty"` Encap *[]byte `keks:"encap,omitempty"` } - -type DEM struct { - A string `keks:"a"` -} - -type Encrypted struct { - DEM DEM `keks:"dem"` - KEM []KEM `keks:"kem"` - Salt uuid.UUID `keks:"salt"` - - Ciphertext *keks.BlobChunked `keks:"ciphertext,omitempty"` -} diff --git a/go/cm/go.mod b/go/cm/go.mod index c5fe918..0fc60d3 100644 --- a/go/cm/go.mod +++ b/go/cm/go.mod @@ -1,6 +1,6 @@ module go.cypherpunks.su/keks/cm -go 1.24 +go 1.24.0 require ( github.com/companyzero/sntrup4591761 v0.0.0-20220309191932-9e0f3af2f07a diff --git a/go/cm/hash/hash.go b/go/cm/hash/hash.go new file mode 100644 index 0000000..2491f4b --- /dev/null +++ b/go/cm/hash/hash.go @@ -0,0 +1,7 @@ +package hash + +type Hashed struct { + Algo []string `keks:"a"` + Typ string `keks:"t"` + Hash [][]byte `keks:"hash"` +} diff --git a/go/cm/utils/mk-bin b/go/cm/utils/mk-bin new file mode 100755 index 0000000..c90d0aa --- /dev/null +++ b/go/cm/utils/mk-bin @@ -0,0 +1,9 @@ +#!/bin/sh -e + +GO_LDFLAGS="${GO_LDFLAGS:--s}" + +mkdir -p bin +for cmd in enc hsh key sig ; do + cmd=${cmd}tool + go build -o bin/cm$cmd -ldflags "$GO_LDFLAGS" ./cmd/$cmd +done diff --git a/go/cmd/pp/go.mod b/go/cmd/pp/go.mod new file mode 100644 index 0000000..b07a10e --- /dev/null +++ b/go/cmd/pp/go.mod @@ -0,0 +1,17 @@ +module go.cypherpunks.su/keks/cmd/pp + +go 1.24.0 + +replace go.cypherpunks.su/keks => ../../ + +require ( + go.cypherpunks.su/keks v0.0.0-00010101000000-000000000000 + go.cypherpunks.su/tai64n/v4 v4.1.0 + golang.org/x/term v0.28.0 +) + +require ( + github.com/google/uuid v1.6.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + golang.org/x/sys v0.29.0 // indirect +) diff --git a/go/cmd/pp/go.sum b/go/cmd/pp/go.sum new file mode 100644 index 0000000..268b767 --- /dev/null +++ b/go/cmd/pp/go.sum @@ -0,0 +1,10 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +go.cypherpunks.su/tai64n/v4 v4.1.0 h1:jW0EyklKXpSy9DSFMcDbu7XuLlMkn6kkpNWiMG6UT5c= +go.cypherpunks.su/tai64n/v4 v4.1.0/go.mod h1:/uKUdhLOy8UciRKpapPaFXSOoa/SiXjs3XsDDpAz7OA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= diff --git a/go/cmd/pp/main.go b/go/cmd/pp/main.go index 532781b..5d0d2b6 100644 --- a/go/cmd/pp/main.go +++ b/go/cmd/pp/main.go @@ -33,7 +33,8 @@ import ( ) var ( - MaxStrLen = flag.Uint("max-str-len", 40, "Maximal string length to print") + MaxStrLen = flag.Uint("max-str-len", 64, + "Maximal string length to print") MaxParseCycles = flag.Uint64("max-parse-cycles", 1<<63, "Maximal number of parse cycles to perform") ) diff --git a/go/go.mod b/go/go.mod index 8240e2d..4533458 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,12 +1,9 @@ module go.cypherpunks.su/keks -go 1.24 +go 1.24.0 require ( github.com/google/uuid v1.6.0 github.com/mitchellh/mapstructure v1.5.0 go.cypherpunks.su/tai64n/v4 v4.1.0 - golang.org/x/term v0.28.0 ) - -require golang.org/x/sys v0.29.0 // indirect diff --git a/go/go.sum b/go/go.sum index 268b767..063c682 100644 --- a/go/go.sum +++ b/go/go.sum @@ -4,7 +4,3 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= go.cypherpunks.su/tai64n/v4 v4.1.0 h1:jW0EyklKXpSy9DSFMcDbu7XuLlMkn6kkpNWiMG6UT5c= go.cypherpunks.su/tai64n/v4 v4.1.0/go.mod h1:/uKUdhLOy8UciRKpapPaFXSOoa/SiXjs3XsDDpAz7OA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= diff --git a/go/utils/mk-bin b/go/utils/mk-bin new file mode 100755 index 0000000..133b75f --- /dev/null +++ b/go/utils/mk-bin @@ -0,0 +1,8 @@ +#!/bin/sh -e + +GO_LDFLAGS="${GO_LDFLAGS:--s}" + +mkdir -p bin +bin=$(realpath bin) +cd cmd/pp +go build -o $bin/kekspp -ldflags "$GO_LDFLAGS" diff --git a/spec/cm/encrypted.cddl b/spec/cm/encrypted.cddl index 45300ce..67e5a93 100644 --- a/spec/cm/encrypted.cddl +++ b/spec/cm/encrypted.cddl @@ -4,7 +4,7 @@ cm-encrypted = { dem: dem, kem: [+ kem], salt: uuid, - ? ciphertext: blob, + ? payload: bytes, } dem = dem-chacha20poly1305 / dem-kuznechik-ctracpkm-hmac diff --git a/spec/cm/encrypted.texi b/spec/cm/encrypted.texi index c74e041..912d51a 100644 --- a/spec/cm/encrypted.texi +++ b/spec/cm/encrypted.texi @@ -7,15 +7,16 @@ Stored in a file, it should begin with "cm/encrypted" @ref{MAGIC, magic}. @verbatiminclude cm/encrypted.cddl -@code{/ciphertext} contains the ciphertext. It is encrypted with random +@code{/payload} contains the ciphertext. It is encrypted with random "content encryption key" (CEK) with an algorithm specified in @code{/dem/a} (data encapsulation mechanism). @code{/dem} may contain additional fields supplementing the decryption process, like initialisation vector. -@code{/ciphertext} is a BLOB, which chunk's length depends on DEM -algorithm. If it is absent, then ciphertext is provided by other means, -for example just following the @code{cm-encrypted} structure. +If @code{/payload} is absent, then ciphertext is provided by other +means, for example just by following the @code{cm-encrypted} structure. +It is recommended to encode it as a BLOB, which chunk's length depends +on DEM algorithm. CEK is encapsulated in @code{/kem/*} entries (key encapsulation mechanism), using @code{/kem/*/a} algorithm. @code{/kem/*/cek} field @@ -54,7 +55,7 @@ ChaCha20-Poly1305(key=KEY, ad="", 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{/ciphertext}'s chunk length equals to 32+128KiB+16 bytes. + @code{/payload}'s chunk length equals to 32+128KiB+16 bytes. @node cm-encrypted-kuznechik-ctracpkm-hmac @cindex cm-encrypted-kuznechik-ctracpkm-hmac @@ -73,7 +74,7 @@ Kenc || IV || Kauth = CEK size and IV initialisation vector. Authentication of ciphertext is performed with Streebog-512 (ГОСТ Р 34.11-2012) in HMAC mode. - @code{/ciphertext}'s chunk length equals to 128KiB bytes. + @code{/payload}'s chunk length equals to 128KiB bytes. @node cm-encrypted-balloon-blake2b-hkdf @cindex cm-encrypted-balloon-blake2b-hkdf diff --git a/spec/cm/hashed.cddl b/spec/cm/hashed.cddl index 8195590..ac3a220 100644 --- a/spec/cm/hashed.cddl +++ b/spec/cm/hashed.cddl @@ -3,6 +3,6 @@ ai = text ; algorithm identifier cm-hashed = { a: [+ ai], t: text, ; type of the content - v: bytes / text / blob / map / list, ; content itself + ? v: bytes / blob, ; content itself hash: [+ bytes], ; hash values } diff --git a/spec/cm/hashed.texi b/spec/cm/hashed.texi index e0112d7..fd5e93f 100644 --- a/spec/cm/hashed.texi +++ b/spec/cm/hashed.texi @@ -14,11 +14,6 @@ Stored in a file, it should begin with "cm/hashed" @ref{MAGIC, magic}. @code{/t} tells the type of the data inside. -If @code{/v} is either a MAP or LIST, then its encoded binary -representation is hashed. If it is BIN/STR or BLOB, then its binary -contents are hashed. So hash will stay the same even if data is -converted from BIN to BLOB. - @code{/hash} contains the hash values for all corresponding @code{/a} algorithms.