]> Cypherpunks repositories - keks.git/commitdiff
Support of encrypted private signing keys
authorSergey Matveev <stargrave@stargrave.org>
Thu, 5 Jun 2025 09:46:54 +0000 (12:46 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 5 Jun 2025 09:46:54 +0000 (12:46 +0300)
go/cm/cmd/cmenctool/main.go
go/cm/cmd/cmenctool/passphrase.t
go/cm/cmd/cmenctool/prv-encrypted.t
go/cm/cmd/cmenctool/pub.t
go/cm/cmd/cmsigtool/main.go
go/cm/enc/balloon/possible.go [new file with mode: 0644]
go/cm/enc/passwd.go [new file with mode: 0644]

index 76bc585c7ead95b0e93c2e1571c9e7ade030bd1706197c773d039c9fdf8d029d..0f0022c74810f6d6c6cd3c858011d282732734d7e6f542f9367283bc21d5f0d7 100644 (file)
@@ -24,7 +24,6 @@ import (
        "crypto/sha3"
        "errors"
        "flag"
-       "fmt"
        "hash"
        "io"
        "log"
@@ -35,7 +34,6 @@ import (
        "go.cypherpunks.su/balloon/v3"
        "golang.org/x/crypto/blake2b"
        "golang.org/x/crypto/chacha20poly1305"
-       "golang.org/x/term"
 
        "go.cypherpunks.su/keks"
        "go.cypherpunks.su/keks/cm"
@@ -69,93 +67,33 @@ func blake2bHash() hash.Hash {
        return h
 }
 
-func readPasswd(prompt string) (passwd []byte) {
-       if raw := os.Getenv("CMENCTOOL_PASSPHRASE"); raw != "" {
-               return []byte(raw)
-       }
-       tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
-       if err != nil {
-               log.Fatal(err)
-       }
-       defer tty.Close()
-       tty.WriteString(prompt)
-       passwd, err = term.ReadPassword(int(tty.Fd()))
+func parsePrv(data []byte) (av cm.AV, tail []byte, err error) {
+       data, err = cmballoon.PossibleInteractiveDecrypt(data)
        if err != nil {
-               log.Fatalln(err)
+               return
        }
-       // taken from age/cmd/age/tui.go:clearLine
-       const (
-               CUI = "\033["   // Control Sequence Introducer
-               CPL = CUI + "F" // Cursor Previous Line
-               EL  = CUI + "K" // Erase in Line
-       )
-       fmt.Fprintf(tty, "\r\n"+CPL+EL)
-       return
-}
-
-func parsePrv(data []byte) (av cm.AV, tail []byte, err error) {
-       var magic keks.Magic
-       magic, data = keks.StripMagic(data)
-       switch magic {
-       case sign.PrvMagic:
-       case cmenc.Magic:
-               var encrypted cmenc.Encrypted
-               var v any
-               {
-                       d := keks.NewDecoderFromBytes(data, nil)
-                       v, err = d.Decode()
-                       if err != nil {
-                               return
-                       }
-                       err = schema.Check("encrypted", cmenc.EncryptedSchemas, v)
-                       if err != nil {
-                               return
-                       }
-                       err = d.UnmarshalStruct(&encrypted)
-                       if err != nil {
-                               return
-                       }
-               }
-               if encrypted.DEM.A != chapoly.DEMAlgo {
-                       err = errors.New("unsupported prv encryption DEM")
-                       return
-               }
-               if len(encrypted.KEM) != 1 ||
-                       encrypted.KEM[0].A != cmballoon.BalloonBLAKE2bHKDF ||
-                       len(encrypted.Payload) == 0 {
-                       err = errors.New("wrong prv encryption KEM")
-                       return
-               }
-               v = v.(map[string]any)["kem"].([]any)[0]
-               err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, v)
-               if err != nil {
+       {
+               var magic keks.Magic
+               magic, data = keks.StripMagic(data)
+               if magic != sign.PrvMagic {
+                       err = errors.New("wrong magic")
                        return
                }
-
-               passwd := readPasswd("Passphrase for private key:")
-               var cek []byte
-               cek, err = cmballoon.Decapsulate(encrypted.KEM[0], encrypted.Id[:], passwd)
-               if err != nil {
+       }
+       sign.PrvParse(data)
+       d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16})
+       {
+               var v any
+               if v, err = d.Decode(); err != nil {
                        return
                }
-               var buf bytes.Buffer
-               _, err = chapoly.Open(&buf, bytes.NewReader(encrypted.Payload), cek, 1)
-               if err != nil {
+               if err = schema.Check("av", sign.PubSchemas, v); err != nil {
                        return
                }
-               data = buf.Bytes()
-               magic, data = keks.StripMagic(data)
-               if magic == sign.PrvMagic {
-                       break
-               }
-               fallthrough
-       default:
-               err = errors.New("wrong magic")
+       }
+       if err = d.UnmarshalStruct(&av); err != nil {
                return
        }
-       sign.PrvParse(data)
-       d := keks.NewDecoderFromBytes(data, &keks.DecodeOpts{MaxStrLen: 1 << 16})
-       err = d.DecodeStruct(&av)
        tail = d.B
        return
 }
@@ -275,7 +213,11 @@ func main() {
                                        log.Println(kemIdx, kem.A, "skipping because no -passwd")
                                        continue
                                }
-                               passwd := readPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:")
+                               var passwd []byte
+                               passwd, err = cmenc.ReadPasswd("Passphrase for " + strconv.Itoa(kemIdx) + " KEM:")
+                               if err != nil {
+                                       log.Fatal(err)
+                               }
                                cek, err = cmballoon.Decapsulate(kem, encrypted.Id[:], passwd)
                                if err != nil {
                                        log.Print(err)
@@ -542,9 +484,17 @@ func main() {
                cek = make([]byte, chapoly.CEKLen)
                rand.Read(cek)
                if *passphrase {
-                       passwd := readPasswd("Passphrase:")
+                       var passwd []byte
+                       passwd, err = cmenc.ReadPasswd("Passphrase:")
+                       if err != nil {
+                               log.Fatal(err)
+                       }
                        {
-                               confirm := readPasswd("Confirm:")
+                               var confirm []byte
+                               confirm, err = cmenc.ReadPasswd("Confirm:")
+                               if err != nil {
+                                       log.Fatal(err)
+                               }
                                if !bytes.Equal(passwd, confirm) {
                                        log.Fatal("passphrases do not match")
                                }
index 9ef833ffe9a5863838630c4108b4f1b31f3f44b6a1357e726a2638e676189cba..d1b27ca372404979a2d496049018fd619863b51bf080e3b62a70b9359f7bfb4f 100755 (executable)
@@ -6,7 +6,7 @@ test_description="Check passphrase encryption"
 TMPDIR=${TMPDIR:-/tmp}
 
 dd if=/dev/urandom of=$TMPDIR/enc.data bs=300K count=1 2>/dev/null
-export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p)
+export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p)
 balloonparams="-balloon-s 123 -balloon-t 2"
 test_expect_success "encrypting" "cmenctool $balloonparams -p \
     <$TMPDIR/enc.data >$TMPDIR/enc.enc"
index 48ad4e17770f63e9e1c44d5c7a931a31ef5091266f859b766edc33880b27d62a..e14b25b22a59ed286b6c12198cf3f747f0ef5e0d0c64aab7919cda6747e676f7 100755 (executable)
@@ -7,7 +7,7 @@ TMPDIR=${TMPDIR:-/tmp}
 
 cmkeytool -algo sntrup761-x25519 -ku kem -sub A=KEY 5>$TMPDIR/enc.pub 9>$TMPDIR/enc.prv
 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)
+export CM_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 -embed $balloonparams \
     <$TMPDIR/enc.prv >$TMPDIR/enc.prv.enc"
index c1bc7b87b6f104ffe8d0bb6960f1666eeea98862bc6814a565f4b5f358de772b..322a30c87308d9cb660c32450fa45c254110608d68a612bd029fdb9be561451e 100755 (executable)
@@ -39,7 +39,7 @@ test_expect_success "$algo1: decrypting" "cmenctool -d \
 test_expect_success "$algo1: comparing" \
     "test_cmp $TMPDIR/enc.data $TMPDIR/enc.data.got"
 
-export CMENCTOOL_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p)
+export CM_PASSPHRASE=$(dd if=/dev/urandom bs=32 count=1 2>/dev/null | xxd -p)
 test_expect_success "encrypting also with passphrase" "
     cat $TMPDIR/enc.$algo0.pub $TMPDIR/enc.$algo1.pub |
     cmenctool $balloonparams -p 4<&0 <$TMPDIR/enc.data >$TMPDIR/enc.enc"
index e57c650b400467027d2b0b7ff74a399def7116e24f5b2f33484c87a3a0fe335e..81a48b892e327e7bccab85c570452ae9e188e35f4b5b5fde212efcf817f58603 100644 (file)
@@ -27,6 +27,7 @@ import (
        "time"
 
        "go.cypherpunks.su/keks"
+       cmballoon "go.cypherpunks.su/keks/cm/enc/balloon"
        cmhash "go.cypherpunks.su/keks/cm/hash"
        "go.cypherpunks.su/keks/cm/sign"
        "go.cypherpunks.su/keks/cm/sign/mode"
@@ -215,8 +216,15 @@ func main() {
                }
        } else {
                var signer sign.Iface
-               signer, _, err = sign.PrvParse(mustReadAll(fdPrvR))
-               fdPrvR.Close()
+               {
+                       prvRaw := mustReadAll(fdPrvR)
+                       fdPrvR.Close()
+                       prvRaw, err = cmballoon.PossibleInteractiveDecrypt(prvRaw)
+                       if err != nil {
+                               log.Fatal(err)
+                       }
+                       signer, _, err = sign.PrvParse(prvRaw)
+               }
                if err != nil {
                        log.Fatal(err)
                }
diff --git a/go/cm/enc/balloon/possible.go b/go/cm/enc/balloon/possible.go
new file mode 100644 (file)
index 0000000..0c9f638
--- /dev/null
@@ -0,0 +1,81 @@
+// 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 balloon
+
+import (
+       "bytes"
+       "errors"
+
+       "go.cypherpunks.su/keks"
+       cmenc "go.cypherpunks.su/keks/cm/enc"
+       "go.cypherpunks.su/keks/cm/enc/chapoly"
+       "go.cypherpunks.su/keks/schema"
+)
+
+// Possibly interactively decrypt passphrase-encrypted data.
+func PossibleInteractiveDecrypt(in []byte) (out []byte, err error) {
+       var magic keks.Magic
+       magic, out = keks.StripMagic(in)
+       if magic != cmenc.Magic {
+               return in, nil
+       }
+       var encrypted cmenc.Encrypted
+       var v any
+       {
+               d := keks.NewDecoderFromBytes(out, nil)
+               v, err = d.Decode()
+               if err != nil {
+                       return
+               }
+               err = schema.Check("encrypted", cmenc.EncryptedSchemas, v)
+               if err != nil {
+                       return
+               }
+               err = d.UnmarshalStruct(&encrypted)
+               if err != nil {
+                       return
+               }
+       }
+       if encrypted.DEM.A != chapoly.DEMAlgo {
+               err = errors.New("unsupported encryption DEM")
+               return
+       }
+       if len(encrypted.KEM) != 1 ||
+               encrypted.KEM[0].A != BalloonBLAKE2bHKDF ||
+               len(encrypted.Payload) == 0 {
+               err = errors.New("wrong encryption KEM")
+               return
+       }
+       v = v.(map[string]any)["kem"].([]any)[0]
+       err = schema.Check("kem-balloon-blake2b-hkdf", cmenc.EncryptedSchemas, v)
+       if err != nil {
+               return
+       }
+       var passwd []byte
+       passwd, err = cmenc.ReadPasswd("Passphrase:")
+       if err != nil {
+               return
+       }
+       var cek []byte
+       cek, err = 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)
+       out = buf.Bytes()
+       return
+}
diff --git a/go/cm/enc/passwd.go b/go/cm/enc/passwd.go
new file mode 100644 (file)
index 0000000..167287e
--- /dev/null
@@ -0,0 +1,48 @@
+// 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 encrypted
+
+import (
+       "fmt"
+       "os"
+
+       "golang.org/x/term"
+)
+
+func ReadPasswd(prompt string) (passwd []byte, err error) {
+       if raw := os.Getenv("CM_PASSPHRASE"); raw != "" {
+               return []byte(raw), nil
+       }
+       var tty *os.File
+       tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0)
+       if err != nil {
+               return
+       }
+       defer tty.Close()
+       tty.WriteString(prompt)
+       passwd, err = term.ReadPassword(int(tty.Fd()))
+       if err != nil {
+               return
+       }
+       // taken from age/cmd/age/tui.go:clearLine
+       const (
+               CUI = "\033["   // Control Sequence Introducer
+               CPL = CUI + "F" // Cursor Previous Line
+               EL  = CUI + "K" // Erase in Line
+       )
+       fmt.Fprintf(tty, "\r\n"+CPL+EL)
+       return
+}