From: Sergey Matveev Date: Fri, 14 Feb 2025 13:16:31 +0000 (+0300) Subject: Parallelised ChaPoly X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=b94508bbaeecc5fdc6dbb473bc3381a73e89a80b93fc2b702eb6f7c369348681;p=keks.git Parallelised ChaPoly --- diff --git a/go/atom-encode.go b/go/atom-encode.go index 25a0139..59206b2 100644 --- a/go/atom-encode.go +++ b/go/atom-encode.go @@ -128,16 +128,19 @@ func BigIntEncode(w io.Writer, v *big.Int) (written int64, err error) { } // Write an encoded BLOB atom. +func BlobAtomEncode(w io.Writer, chunkLen int64) (written int64, err error) { + l := make([]byte, 9) + l[0] = byte(AtomBLOB) + be.Put(l[1:], uint64(chunkLen-1)) + return io.Copy(w, bytes.NewReader(l)) +} + +// Write an encoded BLOB. func BlobEncode(w io.Writer, chunkLen int64, r io.Reader) (written int64, err error) { if chunkLen == 0 { return 0, errors.New("zero chunkLen specified") } - { - l := make([]byte, 9) - l[0] = byte(AtomBLOB) - be.Put(l[1:], uint64(chunkLen-1)) - written, err = io.Copy(w, bytes.NewReader(l)) - } + written, err = BlobAtomEncode(w, chunkLen) if err != nil { return } diff --git a/go/blob.go b/go/blob.go index 017624b..8f1c71c 100644 --- a/go/blob.go +++ b/go/blob.go @@ -101,6 +101,7 @@ func (d *BlobDecoder) Next() (chunk []byte, err error) { d.d.deTail() d.d.strs = d.d.strs[:len(d.d.strs)-1] chunk = unsafe.Slice(unsafe.StringData(s), len(s)) + d.d.readBuf = d.d.readBuf[:max(d.ChunkLen, 8)] if int64(len(s)) == d.ChunkLen { return chunk, nil } diff --git a/go/cm/cmd/enctool/chapoly.go b/go/cm/cmd/enctool/chapoly.go deleted file mode 100644 index 5ebe5d3..0000000 --- a/go/cm/cmd/enctool/chapoly.go +++ /dev/null @@ -1,192 +0,0 @@ -// enctool -- dealing with KEKS-encoded cm-encrypted utility -// 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 main - -import ( - "bufio" - "crypto/subtle" - "errors" - "io" - "os" - - "go.cypherpunks.su/keks" - "golang.org/x/crypto/chacha20poly1305" -) - -const ChaPolyChunkLen = 128 * 1024 - -var ChaPolyPad = make([]byte, 32) - -func incr(data []byte) { - for i := len(data) - 1; i >= 0; i-- { - data[i]++ - if data[i] != 0 { - return - } - } -} - -func demChaPolySeal(cek []byte) error { - if len(cek) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { - return errors.New("wrong CEK len") - } - key, iv := cek[:chacha20poly1305.KeySize], cek[chacha20poly1305.KeySize:] - ciph, err := chacha20poly1305.New(key) - if err != nil { - return err - } - in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen) - out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead()) - br := bufio.NewReaderSize(os.Stdin, ChaPolyChunkLen) - bw := bufio.NewWriterSize(os.Stdout, len(out)) - pr, pw := io.Pipe() - blobErr := make(chan error) - go func() { - _, e := keks.BlobEncode(bw, int64(len(out)), pr) - blobErr <- e - }() - ctr := make([]byte, ciph.NonceSize()) - nonce := make([]byte, ciph.NonceSize()) - var n int - var eof bool - for !eof { - n, err = io.ReadFull(br, in[len(ChaPolyPad):]) - if err != nil { - if err != io.ErrUnexpectedEOF { - return err - } - eof = true - } - incr(ctr[:len(ctr)-1]) - if n != ChaPolyChunkLen { - ctr[len(ctr)-1] = 0x01 - } - subtle.XORBytes(nonce, ctr, iv) - _, err = pw.Write(ciph.Seal(out[:0], nonce, in[:len(ChaPolyPad)+n], nil)) - if err != nil { - return err - } - } - if err = pw.Close(); err != nil { - return err - } - if err = <-blobErr; err != nil { - return err - } - return bw.Flush() -} - -func demChaPolyOpen(cek []byte) error { - if len(cek) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { - return errors.New("wrong CEK len") - } - key, iv := cek[:chacha20poly1305.KeySize], cek[chacha20poly1305.KeySize:] - ciph, err := chacha20poly1305.New(key) - if err != nil { - return err - } - in := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen+ciph.Overhead()) - pr, pw := io.Pipe() - blobErr := make(chan error) - go func() { - d, e := keks.NewBlobDecoder( - bufio.NewReaderSize(os.Stdin, len(in)), - int64(len(in)), - ) - defer func() { - pw.Close() - blobErr <- e - }() - if e != nil { - return - } - var chunk []byte - for { - chunk, e = d.Next() - if e != nil { - if e == io.EOF { - e = nil - break - } - return - } - if _, e = pw.Write(chunk); e != nil { - return - } - } - }() - out := make([]byte, len(ChaPolyPad)+ChaPolyChunkLen) - var n int - var eof bool - var chunk []byte - bw := bufio.NewWriterSize(os.Stdout, ChaPolyChunkLen) - ctr := make([]byte, ciph.NonceSize()) - nonce := make([]byte, ciph.NonceSize()) - for !eof { - n, err = io.ReadFull(pr, in) - if err != nil { - if err != io.ErrUnexpectedEOF { - return err - } - eof = true - } - incr(ctr[:len(ctr)-1]) - if n != len(in) { - ctr[len(ctr)-1] = 0x01 - } - subtle.XORBytes(nonce, ctr, iv) - chunk, err = ciph.Open(out[:0], nonce, in[:n], nil) - if err != nil { - return err - } - if subtle.ConstantTimeCompare(chunk[:len(ChaPolyPad)], ChaPolyPad) != 1 { - return errors.New("bad pad") - } - if _, err = bw.Write(chunk[len(ChaPolyPad):]); err != nil { - return err - } - } - if err = <-blobErr; err != nil { - return err - } - return bw.Flush() -} - -func kemChaPolySeal(kek, cek []byte) ([]byte, error) { - ciph, err := chacha20poly1305.New(kek) - if err != nil { - return nil, err - } - nonce := make([]byte, ciph.NonceSize()) - return ciph.Seal(nil, nonce, cek, nil), nil -} - -func kemChaPolyOpen(kek, ciphertext []byte, cekLenExpected int) ([]byte, error) { - ciph, err := chacha20poly1305.New(kek) - if err != nil { - return nil, err - } - nonce := make([]byte, ciph.NonceSize()) - var cek []byte - cek, err = ciph.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - if len(cek) != cekLenExpected { - return nil, errors.New("invalid CEK len") - } - return cek, nil -} diff --git a/go/cm/cmd/enctool/main.go b/go/cm/cmd/enctool/main.go index 42d79a3..c067f9c 100644 --- a/go/cm/cmd/enctool/main.go +++ b/go/cm/cmd/enctool/main.go @@ -27,6 +27,7 @@ import ( "io" "log" "os" + "runtime" "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" + chaPolyDEM "go.cypherpunks.su/keks/cm/enc/chapoly/dem" + chaPolyKEM "go.cypherpunks.su/keks/cm/enc/chapoly/kem" 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" @@ -96,6 +99,7 @@ func main() { balloonT := flag.Int("balloon-t", 3, "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") var pubs []*sign.Pub flag.Func("pub", "Path to public key to encrypt to", func(v string) error { signed, err := sign.PubParse(mustReadFile(v)) @@ -194,7 +198,7 @@ func main() { log.Fatal(err) } var cekp []byte - cekp, err = kemChaPolyOpen( + cekp, err = chaPolyKEM.Open( kek, kem.CEK, chacha20poly1305.KeySize+chacha20poly1305.NonceSize, ) @@ -276,7 +280,7 @@ func main() { log.Fatal(err) } var cekp []byte - cekp, err = kemChaPolyOpen( + cekp, err = chaPolyKEM.Open( kek, kem.CEK, chacha20poly1305.KeySize+chacha20poly1305.NonceSize, ) @@ -369,7 +373,7 @@ func main() { log.Fatal(err) } var cekp []byte - cekp, err = kemChaPolyOpen( + cekp, err = chaPolyKEM.Open( kek[:chacha20poly1305.KeySize], kem.CEK, chacha20poly1305.KeySize+chacha20poly1305.NonceSize, @@ -393,7 +397,7 @@ func main() { if cek == nil { log.Fatal("no KEMs processed") } - err = demChaPolyOpen(cek) + _, err = chaPolyDEM.Open(os.Stdout, os.Stdin, cek, *parallel) if err != nil { log.Fatal(err) } @@ -444,7 +448,7 @@ func main() { if err != nil { log.Fatal(err) } - kem.CEK, err = kemChaPolySeal(kek, cek) + kem.CEK, err = chaPolyKEM.Seal(kek, cek) if err != nil { log.Fatal(err) } @@ -509,7 +513,7 @@ func main() { if err != nil { log.Fatal(err) } - kem.CEK, err = kemChaPolySeal(kek, cek) + kem.CEK, err = chaPolyKEM.Seal(kek, cek) if err != nil { log.Fatal(err) } @@ -575,7 +579,9 @@ func main() { if err != nil { log.Fatal(err) } - kem.CEK, err = kemChaPolySeal(kek[:chacha20poly1305.KeySize], cek) + kem.CEK, err = chaPolyKEM.Seal( + kek[:chacha20poly1305.KeySize], cek, + ) if err != nil { log.Fatal(err) } @@ -607,7 +613,7 @@ func main() { log.Fatal(err) } } - if err = demChaPolySeal(cek); err != nil { + if _, err = chaPolyDEM.Seal(os.Stdout, os.Stdin, cek, *parallel); err != nil { log.Fatal(err) } } diff --git a/go/cm/enc/chapoly/dem/dem.go b/go/cm/enc/chapoly/dem/dem.go new file mode 100644 index 0000000..c8a9033 --- /dev/null +++ b/go/cm/enc/chapoly/dem/dem.go @@ -0,0 +1,212 @@ +// enctool -- dealing with KEKS-encoded cm-encrypted utility +// 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 dem + +import ( + "bytes" + "crypto/cipher" + "crypto/subtle" + "errors" + "io" + + "go.cypherpunks.su/keks" + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + ChaPolyChunkLen = 128 * 1024 + ChaPolyPadLen = 32 +) + +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{} + buf []byte + ctr []byte +} + +func do( + w io.Writer, + seal bool, + r io.Reader, + cek []byte, + procs int, +) (total int64, err error) { + if len(cek) != chacha20poly1305.KeySize+chacha20poly1305.NonceSize { + return 0, errors.New("wrong CEK len") + } + ready := make(chan *job, procs) + dones := make(chan *job, procs) + key, iv := cek[:chacha20poly1305.KeySize], cek[chacha20poly1305.KeySize:] + var ctr []byte + var overhead int + { + var ciph cipher.AEAD + ciph, err = chacha20poly1305.New(key) + if err != nil { + return 0, err + } + ctr = make([]byte, ciph.NonceSize()) + overhead = ciph.Overhead() + } + chunkLen := ChaPolyPadLen + ChaPolyChunkLen + overhead + var blobDecoder *keks.BlobDecoder + if seal { + total, err = keks.BlobAtomEncode(w, int64(chunkLen)) + } else { + blobDecoder, err = keks.NewBlobDecoder(r, int64(chunkLen)) + } + if err != nil { + return + } + chaPolyPad := make([]byte, ChaPolyPadLen) + 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, chunkLen), + ctr: make([]byte, ciph.NonceSize()), + bufReady: make(chan struct{}), + processed: make(chan struct{}), + } + nonce := make([]byte, ciph.NonceSize()) + ready <- &j + var errOpen error + for { + <-j.bufReady + subtle.XORBytes(nonce, j.ctr, iv) + if seal { + ciph.Seal(j.buf[:0], nonce, j.buf[:len(j.buf)-overhead], nil) + } else { + j.buf, errOpen = ciph.Open(j.buf[:0], nonce, j.buf, nil) + if errOpen == nil { + if subtle.ConstantTimeCompare( + j.buf[:ChaPolyPadLen], + chaPolyPad, + ) != 1 { + errUnauth = errors.New("bad pad") + } + } else { + errUnauth = errOpen + } + } + j.processed <- struct{}{} + } + }() + } + finished := make(chan struct{}) + var errW error + go func() { + var n int64 + for j := range dones { + <-j.processed + if seal { + n, errW = keks.BinEncode(w, j.buf) + } else { + if len(j.buf) == 0 { + n = 0 + } else { + n, errW = io.Copy(w, bytes.NewReader(j.buf[ChaPolyPadLen:])) + } + } + total += n + if errW != nil { + break + } + j.buf = j.buf[:chunkLen] + ready <- j + } + close(finished) + }() + var chunk []byte + var errR error + var eof bool + var n int + var j *job + for !eof { + if errUnauth != nil { + err = errUnauth + close(dones) + <-finished + return + } + j = <-ready + if seal { + n, errR = io.ReadFull(r, j.buf[ChaPolyPadLen:ChaPolyPadLen+ChaPolyChunkLen]) + } else { + chunk, errR = blobDecoder.Next() + copy(j.buf, chunk) + n = len(chunk) + j.buf = j.buf[:n] + } + total += int64(n) + if errR != nil { + if !(errR == io.ErrUnexpectedEOF || errR == io.EOF) { + err = errR + close(dones) + <-finished + return + } + errR = nil + eof = true + if seal { + j.buf = j.buf[:ChaPolyPadLen+n+overhead] + } + } + if seal { + clear(j.buf[:ChaPolyPadLen]) + } else if n == 0 { + break + } + if (seal && n < ChaPolyChunkLen) || (!seal && n < chunkLen) { + ctr[len(ctr)-1] = 0x01 + } + incr(ctr[:len(ctr)-1]) + copy(j.ctr, ctr) + j.bufReady <- struct{}{} + dones <- j + } + close(dones) + <-finished + if errUnauth == nil { + err = errW + } else { + err = errUnauth + } + return +} + +func Seal(w io.Writer, r io.Reader, cek []byte, procs int) (total int64, err error) { + return do(w, true, r, cek, procs) +} + +func Open(w io.Writer, r io.Reader, cek []byte, procs int) (total int64, err error) { + return do(w, false, r, cek, procs) +} diff --git a/go/cm/enc/chapoly/kem/kem.go b/go/cm/enc/chapoly/kem/kem.go new file mode 100644 index 0000000..c400ba6 --- /dev/null +++ b/go/cm/enc/chapoly/kem/kem.go @@ -0,0 +1,48 @@ +// 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 kem + +import ( + "errors" + + "golang.org/x/crypto/chacha20poly1305" +) + +func Seal(kek, cek []byte) ([]byte, error) { + ciph, err := chacha20poly1305.New(kek) + if err != nil { + return nil, err + } + nonce := make([]byte, ciph.NonceSize()) + return ciph.Seal(nil, nonce, cek, nil), nil +} + +func Open(kek, ciphertext []byte, cekLenExpected int) ([]byte, error) { + ciph, err := chacha20poly1305.New(kek) + if err != nil { + return nil, err + } + nonce := make([]byte, ciph.NonceSize()) + var cek []byte + cek, err = ciph.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + if len(cek) != cekLenExpected { + return nil, errors.New("invalid CEK len") + } + return cek, nil +}