From 97b386a58ef33cac2ba39ac2e273bc632eb026ad Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 30 Apr 2017 12:20:41 +0300 Subject: [PATCH] Encrypted configuration file --- .gitmodules | 3 + common.mk | 4 + doc/cmds.texi | 45 +++++- doc/download.texi | 1 + doc/eblob.texi | 67 ++++++++ doc/index.texi | 2 + doc/news.ru.texi | 5 + doc/news.texi | 9 +- doc/workflow.texi | 2 +- makedist.sh | 1 + ports/nncp/Makefile | 1 + src/cypherpunks.ru/balloon | 1 + src/cypherpunks.ru/nncp/cfg.go | 19 ++- .../nncp/cmd/nncp-cfgenc/main.go | 126 +++++++++++++++ src/cypherpunks.ru/nncp/eblob.go | 153 ++++++++++++++++++ 15 files changed, 432 insertions(+), 7 deletions(-) create mode 100644 doc/eblob.texi create mode 160000 src/cypherpunks.ru/balloon create mode 100644 src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go create mode 100644 src/cypherpunks.ru/nncp/eblob.go diff --git a/.gitmodules b/.gitmodules index ea445fb..ae68d8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,3 +30,6 @@ [submodule "src/github.com/gorhill/cronexpr"] path = src/github.com/gorhill/cronexpr url = https://github.com/gorhill/cronexpr.git +[submodule "src/cypherpunks.ru/balloon"] + path = src/cypherpunks.ru/balloon + url = git://git.cypherpunks.ru/balloon.git diff --git a/common.mk b/common.mk index 02dee66..327f8df 100644 --- a/common.mk +++ b/common.mk @@ -19,6 +19,7 @@ LDFLAGS = \ ALL = \ nncp-call \ nncp-caller \ + nncp-cfgenc \ nncp-cfgmin \ nncp-cfgnew \ nncp-check \ @@ -42,6 +43,9 @@ nncp-call: nncp-caller: GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-caller +nncp-cfgenc: + GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-cfgenc + nncp-cfgmin: GOPATH=$(GOPATH) go build -ldflags "$(LDFLAGS)" cypherpunks.ru/nncp/cmd/nncp-cfgmin diff --git a/doc/cmds.texi b/doc/cmds.texi index dc4b4ef..039fc2d 100644 --- a/doc/cmds.texi +++ b/doc/cmds.texi @@ -5,8 +5,9 @@ Nearly all commands have the following common options: @table @option @item -cfg - Path to configuration file. May be overrided by @env{NNCPCFG} - environment variable. + Path to configuration file. May be overridden by @env{NNCPCFG} + environment variable. If file file is an encrypted @ref{EBlob, + eblob}, then ask for passphrase to decrypt it first. @item -debug Print debug messages. Normally this option should not be used. @item -minsize @@ -82,6 +83,46 @@ file is renamed from @file{.part} one and when you rerun @command{nncp-call} again, remote node will receive completion notification. +@node nncp-cfgenc +@section nncp-cfgenc + +@verbatim +% nncp-cfgmin [options] [-s INT] [-t INT] [-p INT] cfg.yaml > cfg.yaml.eblob +% nncp-cfgmin [options] -d cfg.yaml.eblob > cfg.yaml +@end verbatim + +This command allows you to encrypt provided @file{cfg.yaml} file with +the passphrase, producing @ref{EBlob, eblob}, to safely keep your +configuration file with private keys. This utility was written for users +who do not want (or can not) to use either @url{https://gnupg.org/, +GnuPG} or similar tools. That @file{eblob} file can be used directly in +@option{-cfg} option of nearly all commands. + +@option{-s}, @option{-t}, @option{-p} are used to tune @file{eblob}'s +password strengthening function. Space memory cost (@option{-s}), +specified in number of BLAKE2b-256 blocks (32 bytes), tells how many +memory must be used for hashing -- bigger values are better, but slower. +Time cost (@option{-t}) tells how many rounds/iterations must be +performed -- bigger is better, but slower. Number of parallel jobs +(@option{-p}) tells how many computation processes will be run: this is +the same as running that number of independent hashers and then joining +their result together. + +When invoked for encryption, passphrase is entered manually twice. When +invoked for decryption (@option{-d} option), it is asked once and exits +if passphrase can not decrypt @file{eblob}. + +@option{-dump} options parses @file{eblob} and prints parameters used +during its creation. For example: +@verbatim +% nncp-cfgenc -dump /usr/local/etc/nncp.yaml.eblob +Strengthening function: Balloon with BLAKE2b-256 +Memory space cost: 1048576 bytes +Number of rounds: 16 +Number of parallel jobs: 2 +Blob size: 2494 +@end verbatim + @node nncp-cfgmin @section nncp-cfgmin diff --git a/doc/download.texi b/doc/download.texi index dbb003c..0c683d5 100644 --- a/doc/download.texi +++ b/doc/download.texi @@ -9,6 +9,7 @@ Tarballs include all necessary required libraries: @multitable @columnfractions .50 .50 @headitem Library @tab Licence +@item @code{cypherpunks.ru/balloon} @tab GNU GPLv3+ @item @code{github.com/dustin/go-humanize} @tab MIT @item @code{github.com/flynn/noise} @tab BSD 3-Clause @item @code{github.com/go-check/check} @tab BSD 2-Clause diff --git a/doc/eblob.texi b/doc/eblob.texi new file mode 100644 index 0000000..baaa718 --- /dev/null +++ b/doc/eblob.texi @@ -0,0 +1,67 @@ +@node EBlob +@unnumbered EBlob format + +Eblob is an encrypted blob (binary large object, in the terms of +databases), holding any kind of symmetrically encrypted data with the +passphrase used to derive the key. It is used to secure configuration +files, holding valuable private keys, allowing them to be transferred +safely everywhere. + +In fact it uses two factors for securing the data: + +@itemize +@item @strong{salt}, that is kept inside @file{eblob}, something @emph{you have} +@item @strong{passphrase}, that is kept inside the head, something @emph{you know} +@end itemize + +Whole security depends on the passphrase itself. Pay attention that this +is @strong{not} the password. Password is a short string of high entropy +(highly random) characters, but passphrase is (very) long string of +low-entropy characters. Low-entropy text is much more easier to +remember, and its length provides pretty enough entropy as a result. + +Password strengthening function is applied to that passphrase to +mitigate brute-force and dictionary attacks on it. Here, +@url{https://crypto.stanford.edu/balloon/, Balloon} memory-hard password +hashing function is used, together with BLAKE2b-256 hash. It has proven +memory-hardness properties, very easy to implement, resistant to cache +attacks and seems more secure than Argon2 +(@url{https://password-hashing.net/, Password Hashing Competition} +winner). + +Eblob is an @url{https://tools.ietf.org/html/rfc4506, XDR}-encoded structure: + +@verbatim ++-------+------------------+------------+ +| MAGIC | S | T | P | SALT | BLOB | MAC | ++-------+------------------+------------+ +@end verbatim + +@multitable @columnfractions 0.2 0.3 0.5 +@headitem @tab XDR type @tab Value +@item Magic number @tab + 8-byte, fixed length opaque data @tab + @verb{|N N C P B 0x00 0x00 0x01|} +@item S, T, P @tab + unsigned integer @tab + Space cost, time cost and parallel jobs number +@item Salt @tab + 32 bytes, fixed length opaque data @tab + Randomly generated salt +@item Blob @tab + variable length opaque data @tab + Encrypted data itself +@item MAC @tab + 32 bytes, fixed length opaque data @tab + BLAKE2b-256 MAC of encrypted blob +@end multitable + +Blob's encryption is done using +@url{https://www.schneier.com/academic/twofish/, Twofish} algorithm with +256-bit key in +@url{https://en.wikipedia.org/wiki/Counter_mode#Counter_.28CTR.29, CTR} +mode of operation with zero initialization vector. +@code{balloon(BLAKE2b-256, S, T, P, salt, password)} gives the main key, +that is fed to @url{https://en.wikipedia.org/wiki/HKDF, +HKDF}-BLAKE2b-256 KDF. Actual encryption key for Twofish and +authentication key for MAC are derived from that KDF. diff --git a/doc/index.texi b/doc/index.texi index 364fc8c..87c008e 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -41,6 +41,7 @@ A copy of the license is included in the section entitled "Copying conditions". * Log format: Log. * Packet format: Packet. * Sync protocol: Sync. +* EBlob format: EBlob. * Thanks:: * Contacts and feedback: Contacts. * Copying conditions: Copying. @@ -62,6 +63,7 @@ A copy of the license is included in the section entitled "Copying conditions". @include log.texi @include pkt.texi @include sp.texi +@include eblob.texi @include thanks.texi @include contacts.texi diff --git a/doc/news.ru.texi b/doc/news.ru.texi index dccde59..2f6bc2e 100644 --- a/doc/news.ru.texi +++ b/doc/news.ru.texi @@ -13,6 +13,7 @@ @command{nncp-reass} командой и @option{freqchunked} опцией конфигурационного файла. Полезно для передачи больших файлов через маленькие устройства хранения. + @item @option{freqminsize} опция конфигурационного файла, аналогичная @option{-minsize}. @@ -29,6 +30,10 @@ а @command{nncp-mincfg} в @command{nncp-cfgmin}, для того чтобы они имели общий префикс и были сгруппированы для удобства. +@item Появилась команда @command{nncp-cfgenc}, позволяющая +шифровать/дешифровать конфигурационный файл, чтобы безопасно его хранить +без использования OpenPGP или других подобных инструментов. + @item Обновлены зависимые криптографические библиотеки. @end itemize diff --git a/doc/news.texi b/doc/news.texi index 5dec744..7bd287c 100644 --- a/doc/news.texi +++ b/doc/news.texi @@ -33,14 +33,19 @@ and @command{nncp-mincfg} to @command{nncp-cfgmin} -- now they have common prefix and are grouped together for convenience. @item -Cryptographic libraries (dependecies) are updated. +@command{nncp-cfgenc} command appeared, allowing configuration file +encryption/decryption, for keeping it safe without any either OpenPGP or +similar tools usage. + +@item +Cryptographic libraries (dependencies) are updated. @end itemize @node Release 0.6 @section Release 0.6 @itemize @item Small @command{nncp-rm} command appeared. -@item Cryptographic libraries (dependecies) are updated. +@item Cryptographic libraries (dependencies) are updated. @end itemize @node Release 0.5 diff --git a/doc/workflow.texi b/doc/workflow.texi index 5ec2c73..137e7f9 100644 --- a/doc/workflow.texi +++ b/doc/workflow.texi @@ -5,7 +5,7 @@ NNCP consists of several utilities. As a rule you will have the following workflow: @enumerate -@item Run @ref{nncp-newcfg} on each node to create an initial +@item Run @ref{nncp-cfgnew} on each node to create an initial @ref{Configuration, configuration} file. @item Tune it up and set at least @ref{Spool, spool} and log paths. @item Share your public keys and reachability addressees with your diff --git a/makedist.sh b/makedist.sh index aeed4e1..a68f436 100755 --- a/makedist.sh +++ b/makedist.sh @@ -39,6 +39,7 @@ golang.org/x/crypto/hkdf golang.org/x/crypto/nacl golang.org/x/crypto/poly1305 golang.org/x/crypto/salsa20 +golang.org/x/crypto/ssh/terminal golang.org/x/crypto/twofish golang.org/x/net/AUTHORS golang.org/x/net/CONTRIBUTORS diff --git a/ports/nncp/Makefile b/ports/nncp/Makefile index 7e2fcb6..3738689 100644 --- a/ports/nncp/Makefile +++ b/ports/nncp/Makefile @@ -28,6 +28,7 @@ INSTALL_TARGET= install-strip PLIST_FILES= bin/nncp-call \ bin/nncp-caller \ + bin/nncp-cfgenc \ bin/nncp-cfgmin \ bin/nncp-cfgnew \ bin/nncp-check \ diff --git a/src/cypherpunks.ru/balloon b/src/cypherpunks.ru/balloon new file mode 160000 index 0000000..2be0740 --- /dev/null +++ b/src/cypherpunks.ru/balloon @@ -0,0 +1 @@ +Subproject commit 2be074075c635f95406490655039988c8e3633d8 diff --git a/src/cypherpunks.ru/nncp/cfg.go b/src/cypherpunks.ru/nncp/cfg.go index 9419886..3400ed8 100644 --- a/src/cypherpunks.ru/nncp/cfg.go +++ b/src/cypherpunks.ru/nncp/cfg.go @@ -19,12 +19,15 @@ along with this program. If not, see . package nncp import ( + "bytes" "errors" + "log" "os" "path" "github.com/gorhill/cronexpr" "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh/terminal" "gopkg.in/yaml.v2" ) @@ -334,9 +337,21 @@ func (nodeOur *NodeOur) ToYAML() string { } func CfgParse(data []byte) (*Ctx, error) { + var err error + if bytes.Compare(data[:8], MagicNNCPBv1[:]) == 0 { + os.Stderr.WriteString("Passphrase:") + password, err := terminal.ReadPassword(0) + if err != nil { + log.Fatalln(err) + } + os.Stderr.WriteString("\n") + data, err = DeEBlob(data, password) + if err != nil { + return nil, err + } + } var cfgYAML CfgYAML - err := yaml.Unmarshal(data, &cfgYAML) - if err != nil { + if err = yaml.Unmarshal(data, &cfgYAML); err != nil { return nil, err } if _, exists := cfgYAML.Neigh["self"]; !exists { diff --git a/src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go b/src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go new file mode 100644 index 0000000..9376d33 --- /dev/null +++ b/src/cypherpunks.ru/nncp/cmd/nncp-cfgenc/main.go @@ -0,0 +1,126 @@ +/* +NNCP -- Node to Node copy, utilities for store-and-forward data exchange +Copyright (C) 2016-2017 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// NNCP configuration file encrypter/decrypter. +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "os" + + "cypherpunks.ru/nncp" + "github.com/davecgh/go-xdr/xdr2" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/ssh/terminal" +) + +func usage() { + fmt.Fprintf(os.Stderr, nncp.UsageHeader()) + fmt.Fprintln(os.Stderr, "nncp-cfgenc -- encrypt/decrypt configuration file\n") + fmt.Fprintf(os.Stderr, "Usage: %s [options] cfg.yaml > cfg.yaml.eblob\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s [options] -d cfg.yaml.eblob > cfg.yaml\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " %s [options] -dump cfg.yaml.eblob\n", os.Args[0]) + fmt.Fprintln(os.Stderr, "Options:") + flag.PrintDefaults() +} + +func main() { + var ( + decrypt = flag.Bool("d", false, "Decrypt the file") + dump = flag.Bool("dump", false, "Print human-readable eblob information") + sOpt = flag.Int("s", nncp.DefaultS, "Balloon space cost, in 32 bytes chunks") + tOpt = flag.Int("t", nncp.DefaultT, "Balloon time cost, number of rounds") + pOpt = flag.Int("p", nncp.DefaultP, "Balloon number of parallel jobs") + version = flag.Bool("version", false, "Print version information") + warranty = flag.Bool("warranty", false, "Print warranty information") + ) + flag.Usage = usage + flag.Parse() + if *warranty { + fmt.Println(nncp.Warranty) + return + } + if *version { + fmt.Println(nncp.VersionGet()) + return + } + + if flag.NArg() != 1 { + usage() + os.Exit(1) + } + + data, err := ioutil.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatalln("Can not read data:", err) + } + if *dump { + var eblob nncp.EBlob + if _, err := xdr.Unmarshal(bytes.NewReader(data), &eblob); err != nil { + log.Fatalln(err) + } + if eblob.Magic != nncp.MagicNNCPBv1 { + log.Fatalln(errors.New("Unknown eblob type")) + } + fmt.Println("Strengthening function: Balloon with BLAKE2b-256") + fmt.Printf("Memory space cost: %d bytes\n", eblob.SCost*blake2b.Size256) + fmt.Printf("Number of rounds: %d\n", eblob.TCost) + fmt.Printf("Number of parallel jobs: %d\n", eblob.PCost) + fmt.Printf("Blob size: %d\n", len(eblob.Blob)) + os.Exit(0) + } + if *decrypt { + os.Stderr.WriteString("Passphrase:") + password, err := terminal.ReadPassword(0) + if err != nil { + log.Fatalln(err) + } + os.Stderr.WriteString("\n") + cfgRaw, err := nncp.DeEBlob(data, password) + if err != nil { + log.Fatalln(err) + } + os.Stdout.Write(cfgRaw) + } else { + os.Stderr.WriteString("Passphrase:") + password1, err := terminal.ReadPassword(0) + if err != nil { + log.Fatalln(err) + } + os.Stderr.WriteString("\n") + os.Stderr.WriteString("Repeat passphrase:") + password2, err := terminal.ReadPassword(0) + if err != nil { + log.Fatalln(err) + } + os.Stderr.WriteString("\n") + if bytes.Compare(password1, password2) != 0 { + log.Fatalln(errors.New("Passphrases do not match")) + } + eblob, err := nncp.NewEBlob(*sOpt, *tOpt, *pOpt, password1, data) + if err != nil { + log.Fatalln(err) + } + os.Stdout.Write(eblob) + } +} diff --git a/src/cypherpunks.ru/nncp/eblob.go b/src/cypherpunks.ru/nncp/eblob.go new file mode 100644 index 0000000..cb241ab --- /dev/null +++ b/src/cypherpunks.ru/nncp/eblob.go @@ -0,0 +1,153 @@ +/* +NNCP -- Node to Node copy, utilities for store-and-forward data exchange +Copyright (C) 2016-2017 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package nncp + +import ( + "bytes" + "crypto/cipher" + "crypto/rand" + "crypto/subtle" + "errors" + "io" + + "cypherpunks.ru/balloon" + "github.com/davecgh/go-xdr/xdr2" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/twofish" +) + +const ( + DefaultS = 1 << 20 / 32 + DefaultT = 1 << 4 + DefaultP = 2 +) + +var ( + MagicNNCPBv1 [8]byte = [8]byte{'N', 'N', 'C', 'P', 'B', 0, 0, 1} +) + +type EBlob struct { + Magic [8]byte + SCost uint32 + TCost uint32 + PCost uint32 + Salt *[32]byte + Blob []byte + MAC *[blake2b.Size256]byte +} + +// Create an encrypted blob. sCost -- memory space requirements, number +// of hash-output sized (32 bytes) blocks. tCost -- time requirements, +// number of rounds. pCost -- number of parallel jobs. +func NewEBlob(sCost, tCost, pCost int, password, data []byte) ([]byte, error) { + salt := new([32]byte) + var err error + if _, err = rand.Read(salt[:]); err != nil { + return nil, err + } + key := balloon.H(blake256, password, salt[:], sCost, tCost, pCost) + kdf := hkdf.New(blake256, key, nil, MagicNNCPBv1[:]) + keyEnc := make([]byte, 32) + if _, err = io.ReadFull(kdf, keyEnc); err != nil { + return nil, err + } + keyAuth := make([]byte, 64) + if _, err = io.ReadFull(kdf, keyAuth); err != nil { + return nil, err + } + ciph, err := twofish.NewCipher(keyEnc) + if err != nil { + return nil, err + } + ctr := cipher.NewCTR(ciph, make([]byte, twofish.BlockSize)) + mac, err := blake2b.New256(keyAuth) + if err != nil { + return nil, err + } + var blob bytes.Buffer + mw := io.MultiWriter(&blob, mac) + ae := &cipher.StreamWriter{S: ctr, W: mw} + if _, err = ae.Write(data); err != nil { + return nil, err + } + macTag := new([blake2b.Size256]byte) + mac.Sum(macTag[:0]) + eblob := EBlob{ + Magic: MagicNNCPBv1, + SCost: uint32(sCost), + TCost: uint32(tCost), + PCost: uint32(pCost), + Salt: salt, + Blob: blob.Bytes(), + MAC: macTag, + } + var eblobRaw bytes.Buffer + if _, err = xdr.Marshal(&eblobRaw, &eblob); err != nil { + return nil, err + } + return eblobRaw.Bytes(), nil +} + +func DeEBlob(eblobRaw, password []byte) ([]byte, error) { + var eblob EBlob + var err error + if _, err = xdr.Unmarshal(bytes.NewReader(eblobRaw), &eblob); err != nil { + return nil, err + } + if eblob.Magic != MagicNNCPBv1 { + return nil, BadMagic + } + key := balloon.H( + blake256, + password, + eblob.Salt[:], + int(eblob.SCost), + int(eblob.TCost), + int(eblob.PCost), + ) + kdf := hkdf.New(blake256, key, nil, MagicNNCPBv1[:]) + keyEnc := make([]byte, 32) + if _, err = io.ReadFull(kdf, keyEnc); err != nil { + return nil, err + } + keyAuth := make([]byte, 64) + if _, err = io.ReadFull(kdf, keyAuth); err != nil { + return nil, err + } + ciph, err := twofish.NewCipher(keyEnc) + if err != nil { + return nil, err + } + ctr := cipher.NewCTR(ciph, make([]byte, twofish.BlockSize)) + mac, err := blake2b.New256(keyAuth) + if err != nil { + return nil, err + } + var blob bytes.Buffer + tr := io.TeeReader(bytes.NewReader(eblob.Blob), mac) + ae := &cipher.StreamReader{S: ctr, R: tr} + if _, err = io.Copy(&blob, ae); err != nil { + return nil, err + } + if subtle.ConstantTimeCompare(mac.Sum(nil), eblob.MAC[:]) != 1 { + return nil, errors.New("Unauthenticated blob") + } + return blob.Bytes(), nil +} -- 2.44.0