[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
ALL = \
nncp-call \
nncp-caller \
+ nncp-cfgenc \
nncp-cfgmin \
nncp-cfgnew \
nncp-check \
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
@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
@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
@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
--- /dev/null
+@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.
* Log format: Log.
* Packet format: Packet.
* Sync protocol: Sync.
+* EBlob format: EBlob.
* Thanks::
* Contacts and feedback: Contacts.
* Copying conditions: Copying.
@include log.texi
@include pkt.texi
@include sp.texi
+@include eblob.texi
@include thanks.texi
@include contacts.texi
@command{nncp-reass} командой и @option{freqchunked} опцией
конфигурационного файла. Полезно для передачи больших файлов через
маленькие устройства хранения.
+
@item
@option{freqminsize} опция конфигурационного файла, аналогичная
@option{-minsize}.
а @command{nncp-mincfg} в @command{nncp-cfgmin}, для того чтобы они
имели общий префикс и были сгруппированы для удобства.
+@item Появилась команда @command{nncp-cfgenc}, позволяющая
+шифровать/дешифровать конфигурационный файл, чтобы безопасно его хранить
+без использования OpenPGP или других подобных инструментов.
+
@item
Обновлены зависимые криптографические библиотеки.
@end itemize
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
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
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
PLIST_FILES= bin/nncp-call \
bin/nncp-caller \
+ bin/nncp-cfgenc \
bin/nncp-cfgmin \
bin/nncp-cfgnew \
bin/nncp-check \
--- /dev/null
+Subproject commit 2be074075c635f95406490655039988c8e3633d8
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"
)
}
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 {
--- /dev/null
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2017 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// 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)
+ }
+}
--- /dev/null
+/*
+NNCP -- Node to Node copy, utilities for store-and-forward data exchange
+Copyright (C) 2016-2017 Sergey Matveev <stargrave@stargrave.org>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+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
+}