├── encoding └── basex │ ├── gen_test_vectors │ ├── .gitignore │ ├── package.json │ └── gen.iced │ ├── Makefile │ ├── stream_test.go │ ├── bases.go │ ├── basex_test.go │ ├── stream.go │ └── encoding.go ├── basic ├── doc.go └── key_test.go ├── .pre-commit-config.yaml ├── go.mod ├── util_test.go ├── signcrypt_seal_i386_test.go ├── signcrypt_seal_amd64_test.go ├── armor62_signcrypt_test.go ├── specs ├── saltpack.md ├── saltpack_signing_v2.md └── saltpack_signing_v1.md ├── .github └── workflows │ └── ci.yml ├── LICENSE ├── go.sum ├── msgpack.go ├── chunk_reader.go ├── nonce_test.go ├── armor62_sign_test.go ├── doc.go ├── sign.go ├── .golangci.yml ├── armor62_decrypt.go ├── nonce.go ├── signcrypt_open_test.go ├── verify_stream.go ├── punctuated_reader_test.go ├── armor62_sign.go ├── armor62_encrypt.go ├── signcrypt_seal_test.go ├── verify.go ├── frame.go ├── armor62_verify.go ├── armor62_encrypt_test.go ├── armor_test.go ├── README.md ├── armor62.go ├── const.go ├── punctuated_reader.go ├── rand.go ├── decrypt_test.go ├── armor62_signcrypt.go ├── key.go ├── tweakable_signer_test.go ├── verify_test.go ├── errors.go ├── packets_test.go ├── example_test.go ├── packets.go ├── sign_stream.go ├── rand_test.go ├── common_test.go ├── chunk_reader_test.go └── common.go /encoding/basex/gen_test_vectors/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /basic/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package basic is a basic implementation of a saltpack key/keyring configuration. 3 | */ 4 | package basic 5 | -------------------------------------------------------------------------------- /encoding/basex/Makefile: -------------------------------------------------------------------------------- 1 | 2 | vectors_test.go: gen_test_vectors/gen.iced 3 | gen_test_vectors/node_modules/.bin/iced ./$< > $@ 4 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/gabriel/pre-commit-golang 2 | sha: 0efd4687f1c0709db41e144aa89b5b10db7052a1 3 | hooks: 4 | - id: go-fmt 5 | - id: go-metalinter 6 | -------------------------------------------------------------------------------- /encoding/basex/gen_test_vectors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gen_test_vectors", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "armorx": "0.0.2", 13 | "iced-coffee-script": "^108.0.9" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keybase/saltpack 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.25.5 6 | 7 | require ( 8 | github.com/keybase/go-codec v0.0.0-20180928230036-164397562123 9 | github.com/stretchr/testify v1.11.1 10 | golang.org/x/crypto v0.46.0 11 | golang.org/x/sync v0.19.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | golang.org/x/sys v0.39.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func requireErrSuffix(t *testing.T, err error, suffix string) { 14 | require.True(t, strings.HasSuffix(err.Error(), suffix), "err=%v, suffix=%s", err, suffix) 15 | } 16 | 17 | func requireErrContains(t *testing.T, err error, substr string) { 18 | require.True(t, strings.Contains(err.Error(), substr), "err=%v, substr=%s", err, substr) 19 | } 20 | -------------------------------------------------------------------------------- /signcrypt_seal_i386_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | //go:build 386 5 | // +build 386 6 | 7 | package saltpack 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestCheckSigncryptReceiverCount386(t *testing.T) { 16 | maxInt := int(^uint(0) >> 1) 17 | 18 | err := checkSigncryptReceiverCount(maxInt, 1) 19 | require.NoError(t, err) 20 | 21 | err = checkSigncryptReceiverCount(1, maxInt) 22 | require.NoError(t, err) 23 | 24 | err = checkSigncryptReceiverCount(maxInt, maxInt) 25 | require.NoError(t, err) 26 | } 27 | -------------------------------------------------------------------------------- /signcrypt_seal_amd64_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | //go:build amd64 5 | // +build amd64 6 | 7 | package saltpack 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestCheckSigncryptReceiverCountAMD64(t *testing.T) { 16 | err := checkSigncryptReceiverCount(maxReceiverCount, 1) 17 | require.Equal(t, ErrBadReceivers, err) 18 | 19 | err = checkSigncryptReceiverCount(1, maxReceiverCount) 20 | require.Equal(t, ErrBadReceivers, err) 21 | 22 | err = checkSigncryptReceiverCount(maxReceiverCount, maxReceiverCount) 23 | require.Equal(t, ErrBadReceivers, err) 24 | } 25 | -------------------------------------------------------------------------------- /armor62_signcrypt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | ) 10 | 11 | func TestSigncryptArmor62(t *testing.T) { 12 | msg := []byte("hello world") 13 | keyring, receiverBoxKeys := makeKeyringWithOneKey(t) 14 | sender := makeSigningKey(t, keyring) 15 | 16 | ciphertext, err := SigncryptArmor62Seal(msg, ephemeralKeyCreator{}, sender, receiverBoxKeys, nil, ourBrand) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | _, output, brand, err := Dearmor62SigncryptOpen(ciphertext, keyring, nil) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | if !bytes.Equal(msg, output) { 26 | t.Fatalf("bad message back out") 27 | } 28 | brandCheck(t, brand) 29 | } 30 | -------------------------------------------------------------------------------- /encoding/basex/stream_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package basex 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | type fakeReader struct { 14 | b byte 15 | n int 16 | err error 17 | } 18 | 19 | func (r fakeReader) Read(b []byte) (int, error) { 20 | for i := 0; i < r.n; i++ { 21 | b[i] = r.b 22 | } 23 | return r.n, r.err 24 | } 25 | 26 | // TestDecodeReaderError tests that errors are propagated properly 27 | // from the source reader. In particular, if decoder.Read uses 28 | // io.ReadAtLeast, which drops errors if the minimum is met, then this 29 | // will fail. 30 | func TestDecodeReaderError(t *testing.T) { 31 | fakeErr := errors.New("fake error") 32 | encoding := Base58StdEncoding 33 | // The minimum passed to io.ReadAtLeast is encoding.baseXBlockLen. 34 | reader := fakeReader{'1', encoding.baseXBlockLen, fakeErr} 35 | decoder := NewDecoder(Base58StdEncoding, reader) 36 | var buf [100]byte 37 | _, err := decoder.Read(buf[:]) 38 | require.Equal(t, fakeErr, err) 39 | } 40 | -------------------------------------------------------------------------------- /specs/saltpack.md: -------------------------------------------------------------------------------- 1 | # Saltpack 2 | ### An Alternative to the PGP Message Format 3 | 4 | Keybase today supports both PGP and NaCl keys. NaCl is the more modern of the 5 | two; we rely on its high performance for our file system operations, and it's 6 | also much simpler to program. PGP by contrast is mostly a backwards 7 | compatibility option, for users who have existing keys they want to prove, or 8 | who don't want to use Keybase software directly. Our plan is that most new 9 | users without previous PGP experience will use NaCl keys exclusively. 10 | 11 | One issue with that plan is that there isn't a standard encryption/signing 12 | format for NaCl keys, in the style of PGP's ASCII-armored messages, that users 13 | can paste into emails or other websites. We need to invent one. 14 | 15 | We start with some simplifying decisions: 16 | 17 | - We'll use [NaCl](http://nacl.cr.yp.to/) for all the cryptography. 18 | - We'll use [MessagePack](http://msgpack.org/index.html) for all the binary 19 | formatting. 20 | 21 | Thus, "saltpack". The spec is in four parts: 22 | 23 | - [a binary encryption format](saltpack_encryption_v2.md) 24 | - [a binary signcryption format](saltpack_signcryption_v2.md) 25 | - [a binary signing format](saltpack_signing_v2.md) 26 | - [an ASCII armor scheme](saltpack_armor.md) 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | schedule: 11 | # Run daily at 2 AM UTC to check for new vulnerabilities 12 | - cron: "0 2 * * *" 13 | 14 | permissions: 15 | contents: read 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | test: 23 | timeout-minutes: 15 24 | strategy: 25 | matrix: 26 | go-version: [1.25.x, 1.24.x] 27 | os: [ubuntu-latest] 28 | runs-on: ${{ matrix.os }} 29 | steps: 30 | - uses: actions/checkout@v6 31 | with: 32 | persist-credentials: false 33 | 34 | - uses: actions/setup-go@v6 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | cache: true 38 | 39 | - name: golangci-lint 40 | uses: golangci/golangci-lint-action@v9 41 | with: 42 | version: v2.7.2 43 | 44 | - name: Build 45 | run: go build -v ./... 46 | 47 | - name: Run govulncheck 48 | uses: golang/govulncheck-action@v1 49 | with: 50 | go-version-input: ${{ matrix.go-version }} 51 | 52 | - name: Test 53 | run: go test -race ./... 54 | -------------------------------------------------------------------------------- /encoding/basex/gen_test_vectors/gen.iced: -------------------------------------------------------------------------------- 1 | 2 | {prng} = require 'crypto' 3 | {encoding} = require('armorx').encoding.b58 4 | 5 | output_test_encode_vectors = () -> 6 | out = { 7 | empty : "".toString('base64') 8 | } 9 | 10 | for i in [1...100] 11 | for j in [0...20] 12 | b = prng(i) 13 | out["rand_#{i}_#{j}"] = b.toString('base64') 14 | 15 | for i in [1...30] 16 | for j in [0..i] 17 | for k in [0...3] 18 | b = new Buffer (0 for l in [0...j] ) 19 | b = Buffer.concat [ b, prng(i-j) ] 20 | out["zero_pad_#{i}_#{j}_#{k}"] = b.toString('base64') 21 | break if i is j 22 | output out, "testEncodeVectors1" 23 | 24 | output = (out, nm) -> 25 | console.log "" 26 | console.log "var #{nm} = map[string]string{" 27 | for k,v of out 28 | console.log """\t"#{k}" : "#{v}",""" 29 | console.log "}" 30 | 31 | junk_sequence = (prob) -> 32 | badch = " !@#$%^&&*())_+-={}[]:;'<>?,./~`0" 33 | (badch[i%badch.length] while (i = prng(1)[0])/256 < prob).join '' 34 | 35 | junk_inserter = (prob) -> (s) -> (c + junk_sequence(prob) for c in s).join '' 36 | 37 | output_test_decode_vectors = () -> 38 | out = {} 39 | for i in [10...100] by 3 40 | for j in [0...75] by 2 41 | b = prng(i) 42 | ji = junk_inserter j / 100 43 | dec = ji encoding.encode b 44 | out[b.toString('base64')] = dec 45 | output out, "testDecodeVectors1" 46 | 47 | main = () -> 48 | console.log "package basex" 49 | output_test_encode_vectors() 50 | output_test_decode_vectors() 51 | 52 | main() 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Keybase 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/keybase/go-codec v0.0.0-20180928230036-164397562123 h1:yg56lYPqh9suJepqxOMd/liFgU/x+maRPiB30JNYykM= 4 | github.com/keybase/go-codec v0.0.0-20180928230036-164397562123/go.mod h1:r/eVVWCngg6TsFV/3HuS9sWhDkAzGG8mXhiuYA+Z/20= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 8 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 9 | golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= 10 | golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 11 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 12 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 13 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 14 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /msgpack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/keybase/go-codec/codec" 10 | ) 11 | 12 | type encoder interface { 13 | Encode(v interface{}) error 14 | } 15 | 16 | func newEncoder(w io.Writer) encoder { 17 | return codec.NewEncoder(w, codecHandle()) 18 | } 19 | 20 | func encodeToBytes(i interface{}) ([]byte, error) { 21 | var encoded []byte 22 | err := codec.NewEncoderBytes(&encoded, codecHandle()).Encode(i) 23 | return encoded, err 24 | } 25 | 26 | // If p has type {Encryption,Signcryption,Signature}Header, then 27 | // decodeFromBytes would succeed even if the version numbers or mode 28 | // aren't encoded as positive fixnums. Ideally, it would reject those 29 | // as malformed, but there's no easy way to do that. 30 | // 31 | // Similarly, we'd ideally reject strings, byte arrays, or arrays that 32 | // aren't minimally encoded, but there's no easy way to check that 33 | // either. 34 | 35 | func decodeFromBytes(p interface{}, b []byte) error { 36 | return codec.NewDecoderBytes(b, codecHandle()).Decode(p) 37 | } 38 | 39 | type msgpackStream struct { 40 | decoder *codec.Decoder 41 | seqno packetSeqno 42 | } 43 | 44 | func newMsgpackStream(r io.Reader) *msgpackStream { 45 | return &msgpackStream{decoder: codec.NewDecoder(r, codecHandle())} 46 | } 47 | 48 | func (r *msgpackStream) Read(i interface{}) (ret packetSeqno, err error) { 49 | if err = r.decoder.Decode(i); err != nil { 50 | return ret, err 51 | } 52 | ret = r.seqno 53 | r.seqno++ 54 | return ret, nil 55 | } 56 | -------------------------------------------------------------------------------- /encoding/basex/bases.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package basex 5 | 6 | // skipChars are the chars that we allow in the body but can be skipped 7 | // in the below encodings. Note that if a character is listed both in the 8 | // skipChar last and in the alphabet list, it isn't skipped. See the specifics 9 | // of NewEncoding() for more details. 10 | // 11 | // Note that this skip char list is in ASCII order 12 | const b58skipChars = "\t\n\r !\"#$%&'()*+,-./0:;<=>?@IOl[\\]^_`{|}~" 13 | 14 | // Bitcoin-style encoding 15 | const base58EncodeStd = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" 16 | 17 | // Base58StdEncodingStrict is the standard base58-encoding, with Strict mode enforcing 18 | // no foreign characters 19 | var Base58StdEncodingStrict = NewEncoding(base58EncodeStd, 19, "") 20 | 21 | // Base58StdEncoding is the standard base58-encoding. Foreign characters are ignored 22 | // as long as they're from the blessed set. 23 | var Base58StdEncoding = NewEncoding(base58EncodeStd, 19, b58skipChars) 24 | 25 | // Unlike Base64, we put the digits first. 26 | const base62EncodeStd = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 27 | 28 | // Base62StdEncodingStrict is the standard 62-encoding, with a 32-byte input block and, a 29 | // 43-byte output block. Strict mode is on, so no foreign characters. 30 | var Base62StdEncodingStrict = NewEncoding(base62EncodeStd, 32, "") 31 | 32 | // Base62StdEncoding is the standard 62-encoding, with a 32-byte input block and, a 33 | // 43-byte output block. 34 | var Base62StdEncoding = NewEncoding(base62EncodeStd, 32, "\t\n\r >") 35 | -------------------------------------------------------------------------------- /chunk_reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | // chunker is an interface for a type that emits a sequence of 7 | // plaintext chunks. 8 | // 9 | // Implementations should follow exampleChunker in 10 | // chunk_reader_test.go pretty closely. 11 | type chunker interface { 12 | // getNextChunk() returns a plaintext chunk with an error. If 13 | // the chunk is empty, the error must be non-nil. Once 14 | // getNextChunk() returns a non-nil error (which may be 15 | // io.EOF), it can assume that it will never be called again. 16 | getNextChunk() ([]byte, error) 17 | } 18 | 19 | // chunkReader is an io.Reader adaptor for chunker. 20 | type chunkReader struct { 21 | chunker chunker 22 | prevChunk []byte 23 | prevErr error 24 | } 25 | 26 | func newChunkReader(chunker chunker) *chunkReader { 27 | return &chunkReader{chunker: chunker} 28 | } 29 | 30 | func (r *chunkReader) Read(p []byte) (n int, err error) { 31 | // Copy data into p until it is full, or getNextChunk() 32 | // returns a non-nil error. 33 | for { 34 | // Drain r.prevChunk first before checking for an error. 35 | if len(r.prevChunk) > 0 { 36 | copied := copy(p[n:], r.prevChunk) 37 | n += copied 38 | r.prevChunk = r.prevChunk[copied:] 39 | if len(r.prevChunk) > 0 { 40 | // p is full. 41 | return n, nil 42 | } 43 | } 44 | 45 | if r.prevErr != nil { 46 | // r.prevChunk is fully drained, so return the 47 | // error. 48 | return n, r.prevErr 49 | } 50 | 51 | r.prevChunk, r.prevErr = r.chunker.getNextChunk() 52 | if len(r.prevChunk) == 0 && r.prevErr == nil { 53 | panic("empty chunk and nil error") 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /nonce_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestNonceForPayloadKeyBoxV1(t *testing.T) { 11 | nonce1 := nonceForPayloadKeyBox(Version1(), 0) 12 | nonce2 := nonceForPayloadKeyBox(Version1(), 1) 13 | 14 | // The V1 MAC key doesn't depend on the index; this is fixed 15 | // in V2. 16 | if nonce2 != nonce1 { 17 | t.Errorf("nonce2 == %v != nonce1 == %v unexpectedly", nonce2, nonce1) 18 | } 19 | } 20 | 21 | func TestNonceForPayloadKeyBoxV2(t *testing.T) { 22 | nonce1a := nonceForPayloadKeyBoxV2(0) 23 | nonce1b := nonceForPayloadKeyBox(Version2(), 0) 24 | nonce2a := nonceForPayloadKeyBoxV2(1) 25 | nonce2b := nonceForPayloadKeyBox(Version2(), 1) 26 | 27 | if nonce1b != nonce1a { 28 | t.Errorf("nonce1b == %v != nonce1a == %v unexpectedly", nonce1b, nonce1a) 29 | } 30 | 31 | if nonce2b != nonce2a { 32 | t.Errorf("nonce2b == %v != nonce2a == %v unexpectedly", nonce2b, nonce2a) 33 | } 34 | 35 | if nonce2a == nonce1a { 36 | t.Errorf("nonce2a == nonce1a == %v unexpectedly", nonce1a) 37 | } 38 | } 39 | 40 | func TestNonceForMACKeyBoxV2(t *testing.T) { 41 | hash1 := headerHash{0x01} 42 | hash2 := headerHash{0x02} 43 | 44 | nonce1 := nonceForMACKeyBoxV2(hash1, false, 0) 45 | nonce2 := nonceForMACKeyBoxV2(hash2, false, 0) 46 | nonce3 := nonceForMACKeyBoxV2(hash1, true, 0) 47 | nonce4 := nonceForMACKeyBoxV2(hash1, false, 1) 48 | 49 | if nonce2 == nonce1 { 50 | t.Errorf("nonce2 == nonce1 == %v unexpectedly", nonce1) 51 | } 52 | 53 | if nonce3 == nonce1 { 54 | t.Errorf("nonce3 == nonce1 == %v unexpectedly", nonce1) 55 | } 56 | 57 | if nonce4 == nonce1 { 58 | t.Errorf("nonce4 == nonce1 == %v unexpectedly", nonce1) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /armor62_sign_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | ) 10 | 11 | func testSignArmor62(t *testing.T, version Version) { 12 | msg := randomMsg(t, 128) 13 | key := newSigPrivKey(t) 14 | smsg, err := SignArmor62(version, msg, key, ourBrand) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if len(smsg) == 0 { 19 | t.Fatal("SignArmor62 returned no error and no output") 20 | } 21 | 22 | skey, vmsg, brand, err := Dearmor62Verify(SingleVersionValidator(version), smsg, kr) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | brandCheck(t, brand) 27 | if !PublicKeyEqual(skey, key.GetPublicKey()) { 28 | t.Errorf("signer key %x, expected %x", skey.ToKID(), key.GetPublicKey().ToKID()) 29 | } 30 | if !bytes.Equal(vmsg, msg) { 31 | t.Errorf("verified msg '%x', expected '%x'", vmsg, msg) 32 | } 33 | } 34 | 35 | func testSignDetachedArmor62(t *testing.T, version Version) { 36 | msg := randomMsg(t, 128) 37 | key := newSigPrivKey(t) 38 | sig, err := SignDetachedArmor62(version, msg, key, ourBrand) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | if len(sig) == 0 { 43 | t.Fatal("empty sig and no error from SignDetachedArmor62") 44 | } 45 | 46 | skey, brand, err := Dearmor62VerifyDetached(SingleVersionValidator(version), msg, sig, kr) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | brandCheck(t, brand) 51 | if !PublicKeyEqual(skey, key.GetPublicKey()) { 52 | t.Errorf("signer key %x, expected %x", skey.ToKID(), key.GetPublicKey().ToKID()) 53 | } 54 | } 55 | 56 | func TestArmor62Sign(t *testing.T) { 57 | tests := []func(*testing.T, Version){ 58 | testSignArmor62, 59 | testSignDetachedArmor62, 60 | } 61 | runTestsOverVersions(t, "test", tests) 62 | } 63 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package saltpack is an implementation of the saltpack message format. Saltpack 3 | is a light wrapper around Dan Berstein's famous NaCl library. It adds support 4 | for longer messages, streaming input and output of data, multiple recipients 5 | for encrypted messages, and a reasonable armoring format. We intend Saltpack 6 | as a replacement for the PGP messaging format, as it can be used in many of 7 | the same circumstances. However, it is designed to be: (1) simpler; (2) 8 | easier to implement; (3) judicious (perhaps judgmental) in its crypto usage; 9 | (4) fully modern (no CFB mode here); (5) high performance; (6) less bug- 10 | prone; (7) generally unwilling to output unauthenticated data; and (8) easier 11 | to compose with other software in any manner of languages or platforms. 12 | 13 | # Key Management 14 | 15 | Saltpack makes no attempt to manage keys. We assume the wrapping application 16 | has a story for key management. 17 | 18 | # Modes of Operation 19 | 20 | Saltpack supports three modes of operation: encrypted messages, attached 21 | signatures, and detached signatures. Encrypted messages use NaCl's 22 | authenticated public-key encryption; we add repudiable authentication. An 23 | attached signature contains a message and a signature that authenticates it. A 24 | detached signature contains just the signature, and assumes an independent 25 | delievery mechanism for the file (this might come up when distributing an ISO 26 | and separate signature of the file). 27 | 28 | # Encoding 29 | 30 | Saltpack has two encoding modes: binary and armored. In armored mode, saltpack 31 | outputs in Base62-encoding, suitable for publication into any manner of Web 32 | settings without fear of markup-caused mangling. 33 | 34 | # API 35 | 36 | This saltpack library implementation supports two API patterns: streaming and 37 | all-at-once. The former is useful for large files that can't fit into memory; 38 | the latter is more convenient. Both produce the same output. 39 | 40 | # More Info 41 | 42 | See https://saltpack.org 43 | */ 44 | package saltpack 45 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | ) 10 | 11 | // NewSignStream creates a stream that consumes plaintext data. 12 | // It will write out signed data to the io.Writer passed in as 13 | // signedtext. NewSignStream only generates attached signatures. 14 | func NewSignStream(version Version, signedtext io.Writer, signer SigningSecretKey) (stream io.WriteCloser, err error) { 15 | return newSignAttachedStream(version, signedtext, signer) 16 | } 17 | 18 | // Sign creates an attached signature message of plaintext from signer. 19 | func Sign(version Version, plaintext []byte, signer SigningSecretKey) ([]byte, error) { 20 | buf, err := signToStream(version, plaintext, signer, NewSignStream) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return buf.Bytes(), nil 25 | } 26 | 27 | // NewSignDetachedStream creates a stream that consumes plaintext 28 | // data. It will write out a detached signature to the io.Writer 29 | // passed in as detachedsig. 30 | func NewSignDetachedStream(version Version, detachedsig io.Writer, signer SigningSecretKey) (stream io.WriteCloser, err error) { 31 | return newSignDetachedStream(version, detachedsig, signer) 32 | } 33 | 34 | // SignDetached returns a detached signature of plaintext from 35 | // signer. 36 | func SignDetached(version Version, plaintext []byte, signer SigningSecretKey) ([]byte, error) { 37 | buf, err := signToStream(version, plaintext, signer, NewSignDetachedStream) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return buf.Bytes(), nil 42 | } 43 | 44 | // signToStream creates a signature for plaintext with signer, 45 | // using streamer to generate a signing stream. 46 | func signToStream(version Version, plaintext []byte, signer SigningSecretKey, streamer func(Version, io.Writer, SigningSecretKey) (io.WriteCloser, error)) (*bytes.Buffer, error) { 47 | var buf bytes.Buffer 48 | s, err := streamer(version, &buf, signer) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if _, err := s.Write(plaintext); err != nil { 53 | return nil, err 54 | } 55 | if err := s.Close(); err != nil { 56 | return nil, err 57 | } 58 | 59 | return &buf, nil 60 | } 61 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | run: 4 | timeout: 5m 5 | tests: true 6 | 7 | formatters: 8 | enable: 9 | - gofumpt 10 | 11 | linters: 12 | enable: 13 | # Core recommended linters 14 | - errcheck # Checks for unchecked errors 15 | - govet # Go vet checks 16 | - ineffassign # Detects ineffectual assignments 17 | - staticcheck # Advanced static analysis 18 | - unused # Finds unused code 19 | 20 | # Code quality 21 | - misspell # Finds commonly misspelled words 22 | - unconvert # Unnecessary type conversions (already enabled in original) 23 | - unparam # Finds unused function parameters 24 | - gocritic # Various checks (already enabled in original) 25 | - revive # Fast, configurable linter (already enabled in original) 26 | 27 | # Security and best practices 28 | - gosec # Security-focused linter 29 | - bodyclose # Checks HTTP response body closed 30 | - noctx # Finds HTTP requests without context 31 | 32 | settings: 33 | gocritic: 34 | disabled-checks: 35 | - ifElseChain 36 | - elseif 37 | 38 | govet: 39 | enable-all: true 40 | disable: 41 | - shadow 42 | - fieldalignment 43 | 44 | revive: 45 | enable-all-rules: false 46 | 47 | exclusions: 48 | rules: 49 | # Exclude specific revive rules 50 | - linters: 51 | - revive 52 | text: "package-comments" 53 | 54 | - linters: 55 | - revive 56 | text: "exported" 57 | 58 | # Exclude specific staticcheck rules 59 | - linters: 60 | - staticcheck 61 | text: "ST1005" 62 | 63 | # Exclude specific gocritic rules 64 | - linters: 65 | - gocritic 66 | text: "ifElseChain" 67 | 68 | # Exclude misspell in test vectors (generated test data) 69 | - linters: 70 | - misspell 71 | path: "encoding/basex/vectors_test.go" 72 | 73 | # Exclude staticcheck suggestions in test/production code 74 | - linters: 75 | - staticcheck 76 | text: "QF1003" 77 | 78 | - linters: 79 | - staticcheck 80 | text: "QF1008" 81 | 82 | # Exclude unparam warnings in test code 83 | - linters: 84 | - unparam 85 | path: "_test\\.go" 86 | -------------------------------------------------------------------------------- /armor62_decrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | ) 10 | 11 | var ( 12 | armor62EncryptionHeaderChecker HeaderChecker = func(header string) (string, error) { 13 | return parseFrame(header, MessageTypeEncryption, headerMarker) 14 | } 15 | armor62EncryptionFrameChecker FrameChecker = func(header, footer string) (string, error) { 16 | return CheckArmor62(header, footer, MessageTypeEncryption) 17 | } 18 | ) 19 | 20 | // NewDearmor62DecryptStream makes a new stream that dearmors and decrypts the given 21 | // Reader stream. Pass it a keyring so that it can lookup private and public keys 22 | // as necessary. Returns the MessageKeyInfo recovered during header 23 | // processing, an io.Reader stream from which you can read the plaintext, the armor branding, and 24 | // maybe an error if there was a failure. 25 | func NewDearmor62DecryptStream(versionValidator VersionValidator, ciphertext io.Reader, kr Keyring) (mki *MessageKeyInfo, ds io.Reader, brand string, err error) { 26 | dearmored, frame, err := NewArmor62DecoderStream(ciphertext, armor62EncryptionHeaderChecker, armor62EncryptionFrameChecker) 27 | if err != nil { 28 | return nil, nil, "", err 29 | } 30 | brand, err = frame.GetBrand() 31 | if err != nil { 32 | return nil, nil, "", err 33 | } 34 | mki, ds, err = NewDecryptStream(versionValidator, dearmored, kr) 35 | if err != nil { 36 | return mki, nil, "", err 37 | } 38 | return mki, ds, brand, nil 39 | } 40 | 41 | // Dearmor62DecryptOpen takes an armor62'ed, encrypted ciphertext and attempts to 42 | // dearmor and decrypt it, using the provided keyring. Checks that the frames in the 43 | // armor are as expected. Returns the MessageKeyInfo recovered during message 44 | // processing, the plaintext (if decryption succeeded), the armor branding, and 45 | // maybe an error if there was a failure. 46 | func Dearmor62DecryptOpen(versionValidator VersionValidator, ciphertext string, kr Keyring) (*MessageKeyInfo, []byte, string, error) { 47 | buf := bytes.NewBufferString(ciphertext) 48 | mki, s, brand, err := NewDearmor62DecryptStream(versionValidator, buf, kr) 49 | if err != nil { 50 | return mki, nil, "", err 51 | } 52 | out, err := io.ReadAll(s) 53 | if err != nil { 54 | return mki, nil, "", err 55 | } 56 | return mki, out, brand, nil 57 | } 58 | -------------------------------------------------------------------------------- /nonce.go: -------------------------------------------------------------------------------- 1 | package saltpack 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | const nonceBytes = 24 8 | 9 | // Nonce is a NaCl-style nonce, with 24 bytes of data, some of which can be 10 | // counter values, and some of which can be random-ish values. 11 | type Nonce [nonceBytes]byte 12 | 13 | func nonceForSenderKeySecretBox() Nonce { 14 | return stringToByte24("saltpack_sender_key_sbox") 15 | } 16 | 17 | func nonceForPayloadKeyBoxV2(recip uint64) Nonce { 18 | var n Nonce 19 | off := len(n) - 8 20 | copyEqualSizeStr(n[:off], "saltpack_recipsb") 21 | binary.BigEndian.PutUint64(n[off:], recip) 22 | return n 23 | } 24 | 25 | func nonceForPayloadKeyBox(version Version, recip uint64) Nonce { 26 | // Switch on the major version since this is called during 27 | // both writing and reading, and in the latter we may 28 | // encounter headers written by unknown minor versions. 29 | switch version.Major { 30 | case 1: 31 | return stringToByte24("saltpack_payload_key_box") 32 | case 2: 33 | return nonceForPayloadKeyBoxV2(recip) 34 | default: 35 | // Let caller be responsible for filtering out unknown 36 | // versions. 37 | panic(ErrBadVersion{version}) 38 | } 39 | } 40 | 41 | func nonceForDerivedSharedKey() Nonce { 42 | return stringToByte24("saltpack_derived_sboxkey") 43 | } 44 | 45 | func nonceForMACKeyBoxV1(headerHash headerHash) Nonce { 46 | return sliceToByte24(headerHash[:nonceBytes]) 47 | } 48 | 49 | func nonceForMACKeyBoxV2(headerHash headerHash, ephemeral bool, recip uint64) Nonce { 50 | var n Nonce 51 | off := len(n) - 8 52 | copyEqualSize(n[:off], headerHash[:off]) 53 | // Set LSB of last byte based on ephemeral. 54 | n[off-1] &^= 1 55 | if ephemeral { 56 | n[off-1] |= 1 57 | } 58 | binary.BigEndian.PutUint64(n[off:], recip) 59 | return n 60 | } 61 | 62 | // Construct the nonce for the ith block of encryption payload. 63 | func nonceForChunkSecretBox(i encryptionBlockNumber) Nonce { 64 | var n Nonce 65 | copyEqualSizeStr(n[0:16], "saltpack_ploadsb") 66 | binary.BigEndian.PutUint64(n[16:], uint64(i)) 67 | return n 68 | } 69 | 70 | // Construct the nonce for the ith block of signcryption 71 | // payload. 72 | func nonceForChunkSigncryption(headerHash headerHash, isFinal bool, i encryptionBlockNumber) Nonce { 73 | var n Nonce 74 | off := len(n) - 8 75 | copyEqualSize(n[:off], headerHash[:off]) 76 | // Set LSB of last byte based on isFinal. 77 | n[off-1] &^= 1 78 | if isFinal { 79 | n[off-1] |= 1 80 | } 81 | binary.BigEndian.PutUint64(n[off:], uint64(i)) 82 | return n 83 | } 84 | 85 | // sigNonce is a nonce for signatures. 86 | type sigNonce [16]byte 87 | 88 | // newSigNonce creates a sigNonce with random bytes. 89 | func newSigNonce() (sigNonce, error) { 90 | var n sigNonce 91 | if err := csprngRead(n[:]); err != nil { 92 | return sigNonce{}, err 93 | } 94 | return n, nil 95 | } 96 | -------------------------------------------------------------------------------- /signcrypt_open_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "io" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestDecryptErrorAtEOF(t *testing.T) { 16 | plaintext := randomMsg(t, 128) 17 | keyring, receiverBoxKeys := makeKeyringWithOneKey(t) 18 | 19 | senderSigningPrivKey := makeSigningKey(t, keyring) 20 | 21 | sealed, err := SigncryptSeal(plaintext, ephemeralKeyCreator{}, senderSigningPrivKey, receiverBoxKeys, nil) 22 | require.NoError(t, err) 23 | 24 | var reader io.Reader = bytes.NewReader(sealed) 25 | errAtEOF := errors.New("err at EOF") 26 | reader = errAtEOFReader{reader, errAtEOF} 27 | _, stream, err := NewSigncryptOpenStream(reader, keyring, nil) 28 | require.NoError(t, err) 29 | 30 | msg, err := io.ReadAll(stream) 31 | requireErrSuffix(t, err, errAtEOF.Error()) 32 | 33 | // Since the bytes are still authenticated, the decrypted 34 | // message should still compare equal to the original input. 35 | require.Equal(t, plaintext, msg) 36 | } 37 | 38 | func TestDecryptNoKey(t *testing.T) { 39 | plaintext := randomMsg(t, 128) 40 | keyring, receiverBoxKeys := makeKeyringWithOneKey(t) 41 | senderSigningPrivKey := makeSigningKey(t, keyring) 42 | 43 | sealed, err := SigncryptSeal(plaintext, ephemeralKeyCreator{}, senderSigningPrivKey, receiverBoxKeys, nil) 44 | require.NoError(t, err) 45 | 46 | // Open with empty keyring 47 | emptyKeyring := makeEmptyKeyring() 48 | sender, msg, openErr := SigncryptOpen(sealed, emptyKeyring, nil) 49 | require.Equal(t, openErr, ErrNoDecryptionKey) 50 | require.Nil(t, sender) 51 | require.Empty(t, msg) 52 | } 53 | 54 | func TestDecryptNoSender(t *testing.T) { 55 | plaintext := randomMsg(t, 128) 56 | 57 | aliceSigningPrivKey := makeSigningSecretKey(t) 58 | 59 | bobKeyring := makeEmptyKeyring() 60 | bobBoxKey, createErr := createEphemeralKey(false) 61 | require.NoError(t, createErr) 62 | bobKeyring.insert(bobBoxKey) 63 | 64 | sealed, err := SigncryptSeal(plaintext, ephemeralKeyCreator{}, aliceSigningPrivKey, []BoxPublicKey{bobBoxKey.GetPublicKey()}, nil) 65 | require.NoError(t, err) 66 | 67 | // Open with only (receiver) key in keyring (not sender) 68 | sender, msg, openErr := SigncryptOpen(sealed, bobKeyring, nil) 69 | require.Equal(t, openErr, ErrNoSenderKey{Sender: aliceSigningPrivKey.GetPublicKey().ToKID()}) 70 | require.Nil(t, sender) 71 | require.Empty(t, msg) 72 | 73 | // Add signing key and try open again 74 | bobKeyring.insertSigningKey(aliceSigningPrivKey) 75 | sender2, msg2, openErr2 := SigncryptOpen(sealed, bobKeyring, nil) 76 | require.NoError(t, openErr2) 77 | require.Equal(t, plaintext, msg2) 78 | require.Equal(t, sender2.ToKID(), aliceSigningPrivKey.GetPublicKey().ToKID()) 79 | } 80 | -------------------------------------------------------------------------------- /verify_stream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "io" 8 | ) 9 | 10 | type verifyStream struct { 11 | mps *msgpackStream 12 | header *SignatureHeader 13 | headerHash headerHash 14 | publicKey SigningPublicKey 15 | } 16 | 17 | func newVerifyStream(versionValidator VersionValidator, r io.Reader, msgType MessageType) (*verifyStream, error) { 18 | s := &verifyStream{ 19 | mps: newMsgpackStream(r), 20 | } 21 | err := s.readHeader(versionValidator, msgType) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return s, nil 26 | } 27 | 28 | func (v *verifyStream) getNextChunk() ([]byte, error) { 29 | signature, chunk, isFinal, seqno, err := readSignatureBlock(v.header.Version, v.mps) 30 | if err != nil { 31 | if err == io.EOF { 32 | err = io.ErrUnexpectedEOF 33 | } 34 | return nil, err 35 | } 36 | 37 | err = v.processBlock(signature, chunk, isFinal, seqno) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | err = checkDecodedChunkState(v.header.Version, chunk, seqno, isFinal) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if isFinal { 48 | return chunk, assertEndOfStream(v.mps) 49 | } 50 | 51 | return chunk, nil 52 | } 53 | 54 | func (v *verifyStream) readHeader(versionValidator VersionValidator, msgType MessageType) error { 55 | var headerBytes []byte 56 | _, err := v.mps.Read(&headerBytes) 57 | if err != nil { 58 | return ErrFailedToReadHeaderBytes 59 | } 60 | 61 | v.headerHash = hashHeader(headerBytes) 62 | 63 | var header SignatureHeader 64 | err = decodeFromBytes(&header, headerBytes) 65 | if err != nil { 66 | return err 67 | } 68 | if err := header.validate(versionValidator, msgType); err != nil { 69 | return err 70 | } 71 | 72 | v.header = &header 73 | return nil 74 | } 75 | 76 | func readSignatureBlock(version Version, mps *msgpackStream) (signature, payloadChunk []byte, isFinal bool, seqno packetSeqno, err error) { 77 | switch version.Major { 78 | case 1: 79 | var sbV1 signatureBlockV1 80 | seqno, err = mps.Read(&sbV1) 81 | if err != nil { 82 | return nil, nil, false, 0, err 83 | } 84 | 85 | return sbV1.Signature, sbV1.PayloadChunk, len(sbV1.PayloadChunk) == 0, seqno, nil 86 | case 2: 87 | var sbV2 signatureBlockV2 88 | seqno, err = mps.Read(&sbV2) 89 | if err != nil { 90 | return nil, nil, false, 0, err 91 | } 92 | 93 | return sbV2.Signature, sbV2.PayloadChunk, sbV2.IsFinal, seqno, nil 94 | default: 95 | panic(ErrBadVersion{version}) 96 | } 97 | } 98 | 99 | func (v *verifyStream) processBlock(signature, payloadChunk []byte, isFinal bool, seqno packetSeqno) error { 100 | return v.publicKey.Verify(attachedSignatureInput(v.header.Version, v.headerHash, payloadChunk, seqno-1, isFinal), signature) 101 | } 102 | -------------------------------------------------------------------------------- /punctuated_reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "testing" 10 | ) 11 | 12 | const testText = `Loving in truth, and fain in verse my love to show 13 | That she (dear She) might take some pleasure of my pain: 14 | Pleasure might cause her read, reading might make her know, 15 | Knowledge might pity win, and pity grace obtain; 16 | I sought fit words to paint the blackest face of woe, 17 | Studying inventions fine, her wits to entertain: 18 | Oft turning others’ leaves, to see if thence would flow 19 | Some fresh and fruitful showers upon my sun-burn’d brain. 20 | But words came halting forth, wanting Invention’s stay, 21 | Invention, Nature’s child, fled step-dame Study’s blows, 22 | And others’ feet still seem’d but strangers in my way. 23 | Thus, great with child to speak, and helpless in my throes, 24 | Biting my truant pen, beating myself for spite-- 25 | “Fool,” said my Muse to me, “look in thy heart and write.” 26 | 27 | Loving, and wishing to show my love in verse, 28 | So that Stella might find pleasure in my pain, 29 | So that pleasure might make her read, and reading make her know me, 30 | And knowledge might win pity for me, and pity might obtain grace, 31 | I looked for fitting words to depict the darkest face of sadness, 32 | Studying clever creations in order to entertain her mind, 33 | Often turning others’ pages to see if, from them, 34 | Fresh and fruitful ideas would flow into my brain. 35 | But words came out lamely, lacking the support of Imagination: 36 | Imagination, nature’s child, fled the blows of Study, her stepmother: 37 | And the writings (‘feet’) of others seemed only alien things in the way. 38 | So while pregnant with the desire to speak, helpless with the birth pangs, 39 | Biting at my pen which disobeyed me, beating myself in anger, 40 | My Muse said to me ‘Fool, look in your heart and write.’ 41 | ` 42 | 43 | func TestPunctuatedReaderRegularReads(t *testing.T) { 44 | buf := bytes.NewBufferString(testText) 45 | r := newPunctuatedReader(buf, '.') 46 | for i := 0; i < 6; i++ { 47 | _, err := r.ReadUntilPunctuation(1024) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | _, err := r.ReadUntilPunctuation(1024) 53 | if err != io.ErrUnexpectedEOF { 54 | t.Fatalf("Wrong error; wanted %v but got %v", io.ErrUnexpectedEOF, err) 55 | } 56 | } 57 | 58 | func TestPunctuatedReaderSlowReads(t *testing.T) { 59 | r := newPunctuatedReader(&slowReader{[]byte(testText)}, '.') 60 | for i := 0; i < 6; i++ { 61 | _, err := r.ReadUntilPunctuation(1024) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | } 66 | _, err := r.ReadUntilPunctuation(1024) 67 | if err != io.ErrUnexpectedEOF { 68 | t.Fatalf("Wrong error; wanted %v but got %v", io.ErrUnexpectedEOF, err) 69 | } 70 | } 71 | 72 | func TestPunctuatedReaderSlowReadsOverflow(t *testing.T) { 73 | r := newPunctuatedReader(&slowReader{[]byte(testText)}, '.') 74 | _, err := r.ReadUntilPunctuation(20) 75 | if err != ErrOverflow { 76 | t.Fatalf("Wrong error; wanted %v but got %v", ErrOverflow, err) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /armor62_sign.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import "io" 7 | 8 | func applyBrand(f func(Version, io.Writer, SigningSecretKey, string) (io.WriteCloser, error), brand string) func(Version, io.Writer, SigningSecretKey) (io.WriteCloser, error) { 9 | return func(version Version, signedtext io.Writer, signer SigningSecretKey) (io.WriteCloser, error) { 10 | return f(version, signedtext, signer, brand) 11 | } 12 | } 13 | 14 | // NewSignArmor62Stream creates a stream that consumes plaintext data. 15 | // It will write out signed data to the io.Writer passed in as 16 | // signedtext. The `brand` is optional, and allows you to specify 17 | // your "white label" brand to this signature. NewSignArmor62Stream only 18 | // generates attached signatures. 19 | // 20 | // The signed data is armored with the recommended armor62-style 21 | // format. 22 | func NewSignArmor62Stream(version Version, signedtext io.Writer, signer SigningSecretKey, brand string) (stream io.WriteCloser, err error) { 23 | enc, err := NewArmor62EncoderStream(signedtext, MessageTypeAttachedSignature, brand) 24 | if err != nil { 25 | return nil, err 26 | } 27 | out, err := NewSignStream(version, enc, signer) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return closeForwarder([]io.WriteCloser{out, enc}), nil 32 | } 33 | 34 | // SignArmor62 creates an attached armored signature message of plaintext from signer. 35 | func SignArmor62(version Version, plaintext []byte, signer SigningSecretKey, brand string) (string, error) { 36 | buf, err := signToStream(version, plaintext, signer, applyBrand(NewSignArmor62Stream, brand)) 37 | if err != nil { 38 | return "", err 39 | } 40 | return buf.String(), nil 41 | } 42 | 43 | // NewSignDetachedArmor62Stream creates a stream that consumes plaintext data. 44 | // It will write out the detached signature to the io.Writer passed in as 45 | // detachedsig. The `brand` is optional, and allows you to specify 46 | // your "white label" brand to this signature. 47 | // 48 | // The signed data is armored with the recommended armor62-style 49 | // NewSignDetachedArmor62Stream only generates detached signatures. 50 | // 51 | // The signature is armored with the recommended armor62-style 52 | // format. 53 | func NewSignDetachedArmor62Stream(version Version, detachedsig io.Writer, signer SigningSecretKey, brand string) (stream io.WriteCloser, err error) { 54 | enc, err := NewArmor62EncoderStream(detachedsig, MessageTypeDetachedSignature, brand) 55 | if err != nil { 56 | return nil, err 57 | } 58 | out, err := NewSignDetachedStream(version, enc, signer) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return closeForwarder([]io.WriteCloser{out, enc}), nil 63 | } 64 | 65 | // SignDetachedArmor62 returns a detached armored signature of plaintext from signer. 66 | func SignDetachedArmor62(version Version, plaintext []byte, signer SigningSecretKey, brand string) (string, error) { 67 | buf, err := signToStream(version, plaintext, signer, applyBrand(NewSignDetachedArmor62Stream, brand)) 68 | if err != nil { 69 | return "", err 70 | } 71 | return buf.String(), nil 72 | } 73 | -------------------------------------------------------------------------------- /armor62_encrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | ) 10 | 11 | type closeForwarder []io.WriteCloser 12 | 13 | func (c closeForwarder) Write(b []byte) (int, error) { 14 | return c[0].Write(b) 15 | } 16 | 17 | func (c closeForwarder) Close() error { 18 | for _, w := range c { 19 | if e := w.Close(); e != nil { 20 | return e 21 | } 22 | } 23 | return nil 24 | } 25 | 26 | func newEncryptArmor62Stream(version Version, ciphertext io.Writer, sender BoxSecretKey, receivers []BoxPublicKey, ephemeralKeyCreator EphemeralKeyCreator, rng encryptRNG, brand string) (plaintext io.WriteCloser, err error) { 27 | enc, err := NewArmor62EncoderStream(ciphertext, MessageTypeEncryption, brand) 28 | if err != nil { 29 | return nil, err 30 | } 31 | out, err := newEncryptStream(version, enc, sender, receivers, ephemeralKeyCreator, rng) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return closeForwarder([]io.WriteCloser{out, enc}), nil 36 | } 37 | 38 | // NewEncryptArmor62Stream creates a stream that consumes plaintext data. 39 | // It will write out encrypted data to the io.Writer passed in as ciphertext. 40 | // The encryption is from the specified sender, and is encrypted for the 41 | // given receivers. 42 | // 43 | // The "brand" is the optional "brand" string to put into the header 44 | // and footer. 45 | // 46 | // The ciphertext is additionally armored with the recommended armor62-style format. 47 | // 48 | // If initialization succeeds, returns an io.WriteCloser that accepts 49 | // plaintext data to be encrypted and a nil error. Otherwise, returns 50 | // nil and the initialization error. 51 | func NewEncryptArmor62Stream(version Version, ciphertext io.Writer, sender BoxSecretKey, receivers []BoxPublicKey, brand string) (plaintext io.WriteCloser, err error) { 52 | ephemeralKeyCreator, err := receiversToEphemeralKeyCreator(receivers) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return newEncryptArmor62Stream(version, ciphertext, sender, receivers, ephemeralKeyCreator, defaultEncryptRNG{}, brand) 57 | } 58 | 59 | func encryptArmor62Seal(version Version, plaintext []byte, sender BoxSecretKey, receivers []BoxPublicKey, ephemeralKeyCreator EphemeralKeyCreator, rng encryptRNG, brand string) (string, error) { 60 | var buf bytes.Buffer 61 | enc, err := newEncryptArmor62Stream(version, &buf, sender, receivers, ephemeralKeyCreator, rng, brand) 62 | if err != nil { 63 | return "", err 64 | } 65 | if _, err := enc.Write(plaintext); err != nil { 66 | return "", err 67 | } 68 | if err := enc.Close(); err != nil { 69 | return "", err 70 | } 71 | return buf.String(), nil 72 | } 73 | 74 | // EncryptArmor62Seal is the non-streaming version of NewEncryptArmor62Stream, which 75 | // inputs a plaintext (in bytes) and output a ciphertext (as a string). 76 | func EncryptArmor62Seal(version Version, plaintext []byte, sender BoxSecretKey, receivers []BoxPublicKey, brand string) (string, error) { 77 | ephemeralKeyCreator, err := receiversToEphemeralKeyCreator(receivers) 78 | if err != nil { 79 | return "", err 80 | } 81 | return encryptArmor62Seal(version, plaintext, sender, receivers, ephemeralKeyCreator, defaultEncryptRNG{}, brand) 82 | } 83 | -------------------------------------------------------------------------------- /signcrypt_seal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestCheckSigncryptReceiverCount(t *testing.T) { 14 | err := checkSigncryptReceiverCount(0, 0) 15 | require.Equal(t, ErrBadReceivers, err) 16 | 17 | err = checkSigncryptReceiverCount(1, 0) 18 | require.NoError(t, err) 19 | 20 | err = checkSigncryptReceiverCount(0, 1) 21 | require.NoError(t, err) 22 | 23 | require.Panics(t, func() { 24 | _ = checkSigncryptReceiverCount(-1, 0) 25 | }) 26 | require.Panics(t, func() { 27 | _ = checkSigncryptReceiverCount(0, -1) 28 | }) 29 | } 30 | 31 | func getSigncryptionReceiverOrder(receivers []receiverKeysMaker) []int { 32 | order := make([]int, len(receivers)) 33 | for i, r := range receivers { 34 | switch r := r.(type) { 35 | case receiverBoxKey: 36 | order[i] = int(r.pk.(boxPublicKey).key[0]) 37 | case ReceiverSymmetricKey: 38 | order[i] = int(r.Key[0]) 39 | } 40 | } 41 | return order 42 | } 43 | 44 | func TestShuffleSigncryptionReceivers(t *testing.T) { 45 | receiverCount := 20 46 | 47 | var receiverBoxKeys []BoxPublicKey 48 | for i := 0; i < receiverCount/2; i++ { 49 | k := boxPublicKey{ 50 | key: RawBoxKey{byte(i)}, 51 | } 52 | receiverBoxKeys = append(receiverBoxKeys, k) 53 | } 54 | 55 | var receiverSymmetricKeys []ReceiverSymmetricKey 56 | for i := receiverCount / 2; i < receiverCount; i++ { 57 | k := ReceiverSymmetricKey{ 58 | Key: SymmetricKey{byte(i)}, 59 | } 60 | receiverSymmetricKeys = append(receiverSymmetricKeys, k) 61 | } 62 | 63 | shuffled, err := shuffleSigncryptReceivers(receiverBoxKeys, receiverSymmetricKeys) 64 | require.NoError(t, err) 65 | 66 | shuffledOrder := getSigncryptionReceiverOrder(shuffled) 67 | require.True(t, isValidNonTrivialPermutation(receiverCount, shuffledOrder), "shuffledOrder == %+v is an invalid or trivial permutation", shuffledOrder) 68 | } 69 | 70 | func TestNewSigncryptSealStreamShuffledReaders(t *testing.T) { 71 | receiverCount := 20 72 | 73 | // Don't include any BoxPublicKeys as it's hard to go from the 74 | // identifier to the index. 75 | 76 | var receiverSymmetricKeys []ReceiverSymmetricKey 77 | for i := 0; i < receiverCount; i++ { 78 | k := ReceiverSymmetricKey{ 79 | Key: SymmetricKey{byte(i)}, 80 | Identifier: []byte{byte(i)}, 81 | } 82 | receiverSymmetricKeys = append(receiverSymmetricKeys, k) 83 | } 84 | 85 | var ciphertext bytes.Buffer 86 | _, err := NewSigncryptSealStream(&ciphertext, ephemeralKeyCreator{}, nil, nil, receiverSymmetricKeys) 87 | require.NoError(t, err) 88 | 89 | var headerBytes []byte 90 | err = decodeFromBytes(&headerBytes, ciphertext.Bytes()) 91 | require.NoError(t, err) 92 | 93 | var header SigncryptionHeader 94 | err = decodeFromBytes(&header, headerBytes) 95 | require.NoError(t, err) 96 | 97 | shuffledOrder := getEncryptReceiverKeysOrder(header.Receivers) 98 | require.True(t, isValidNonTrivialPermutation(receiverCount, shuffledOrder), "shuffledOrder == %+v is an invalid or trivial permutation", shuffledOrder) 99 | } 100 | 101 | // TODO: Add hardcoded signcryption seal/open tests, like 102 | // Test{Seal,Open}HardcodedEncryptMessageV{1,2}. 103 | -------------------------------------------------------------------------------- /verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "crypto/sha512" 9 | "io" 10 | ) 11 | 12 | // NewVerifyStream creates a stream that consumes data from reader 13 | // r. It returns the signer's public key and a reader that only 14 | // contains verified data. If the signer's key is not in keyring, 15 | // it will return an error. 16 | func NewVerifyStream(versionValidator VersionValidator, r io.Reader, keyring SigKeyring) (skey SigningPublicKey, vs io.Reader, err error) { 17 | s, err := newVerifyStream(versionValidator, r, MessageTypeAttachedSignature) 18 | if err != nil { 19 | return nil, nil, err 20 | } 21 | skey = keyring.LookupSigningPublicKey(s.header.SenderPublic) 22 | if skey == nil { 23 | return nil, nil, ErrNoSenderKey{Sender: s.header.SenderPublic} 24 | } 25 | s.publicKey = skey 26 | return skey, newChunkReader(s), nil 27 | } 28 | 29 | // Verify checks the signature in signedMsg. It returns the 30 | // signer's public key and a verified message. 31 | func Verify(versionValidator VersionValidator, signedMsg []byte, keyring SigKeyring) (skey SigningPublicKey, verifiedMsg []byte, err error) { 32 | skey, stream, err := NewVerifyStream(versionValidator, bytes.NewReader(signedMsg), keyring) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | 37 | verifiedMsg, err = io.ReadAll(stream) 38 | if err != nil { 39 | return nil, nil, err 40 | } 41 | return skey, verifiedMsg, nil 42 | } 43 | 44 | // VerifyDetachedReader verifies that signature is a valid signature for 45 | // entire message read from message Reader, and that the public key for 46 | // the signer is in keyring. It returns the signer's public key. 47 | func VerifyDetachedReader(versionValidator VersionValidator, message io.Reader, signature []byte, keyring SigKeyring) (skey SigningPublicKey, err error) { 48 | inputBuffer := bytes.NewBuffer(signature) 49 | 50 | // Use a verifyStream to parse the header. 51 | s, err := newVerifyStream(versionValidator, inputBuffer, MessageTypeDetachedSignature) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | // Reach inside the verifyStream to parse the signature bytes. 57 | var naclSignature []byte 58 | _, err = s.mps.Read(&naclSignature) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // Get the public key. 64 | skey = keyring.LookupSigningPublicKey(s.header.SenderPublic) 65 | if skey == nil { 66 | return nil, ErrNoSenderKey{Sender: s.header.SenderPublic} 67 | } 68 | 69 | // Compute the signed text hash, without requiring us to copy the whole 70 | // signed text into memory at once. 71 | hasher := sha512.New() 72 | _, err = hasher.Write(s.headerHash[:]) 73 | if err != nil { 74 | return nil, err 75 | } 76 | if _, err := io.Copy(hasher, message); err != nil { 77 | return nil, err 78 | } 79 | 80 | if err := skey.Verify(detachedSignatureInputFromHash(hasher.Sum(nil)), naclSignature); err != nil { 81 | return nil, err 82 | } 83 | 84 | return skey, nil 85 | } 86 | 87 | // VerifyDetached verifies that signature is a valid signature for 88 | // message, and that the public key for the signer is in keyring. 89 | // It returns the signer's public key. 90 | func VerifyDetached(versionValidator VersionValidator, message, signature []byte, keyring SigKeyring) (skey SigningPublicKey, err error) { 91 | return VerifyDetachedReader(versionValidator, bytes.NewReader(message), signature, keyring) 92 | } 93 | -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type headerOrFooterMarker string 12 | 13 | const ( 14 | headerMarker headerOrFooterMarker = "BEGIN" 15 | footerMarker headerOrFooterMarker = "END" 16 | maxFrameLength int = 512 // applies to header and footer 17 | maxBrandLength int = 128 18 | ) 19 | 20 | func pop(v *([]string), n int) (ret []string) { 21 | ret = (*v)[(len(*v) - n):] 22 | *v = (*v)[0:(len(*v) - n)] 23 | return 24 | } 25 | 26 | func shift(v *([]string), n int) (ret []string) { 27 | ret = (*v)[0:n] 28 | *v = (*v)[n:] 29 | return 30 | } 31 | 32 | func makeFrame(which headerOrFooterMarker, typ MessageType, brand string) string { 33 | sffx := getStringForType(typ) 34 | if len(sffx) == 0 { 35 | return sffx 36 | } 37 | words := []string{string(which)} 38 | if len(brand) > 0 { 39 | words = append(words, brand) 40 | } 41 | words = append(words, strings.ToUpper(FormatName)) 42 | words = append(words, sffx) 43 | return strings.Join(words, " ") 44 | } 45 | 46 | // MakeArmorHeader makes the armor header for the message type for the given "brand" 47 | func MakeArmorHeader(typ MessageType, brand string) string { 48 | return makeFrame(headerMarker, typ, brand) 49 | } 50 | 51 | // MakeArmorFooter makes the armor footer for the message type for the given "brand" 52 | func MakeArmorFooter(typ MessageType, brand string) string { 53 | return makeFrame(footerMarker, typ, brand) 54 | } 55 | 56 | func getStringForType(typ MessageType) string { 57 | switch typ { 58 | case MessageTypeEncryption: 59 | return EncryptionArmorString 60 | case MessageTypeAttachedSignature: 61 | return SignedArmorString 62 | case MessageTypeDetachedSignature: 63 | return DetachedSignatureArmorString 64 | default: 65 | return "" 66 | } 67 | } 68 | 69 | func parseFrame(m string, typ MessageType, hof headerOrFooterMarker) (brand string, err error) { 70 | if len(m) > maxFrameLength { 71 | err = makeErrBadFrame("Frame is too long") 72 | return 73 | } 74 | 75 | // replace blocks of characters in the set [>\n\r\t ] with a single space, so that Go 76 | // can easily parse each piece 77 | re := regexp.MustCompile("[>\n\r\t ]+") 78 | s := strings.TrimSpace(re.ReplaceAllString(m, " ")) 79 | 80 | sffx := getStringForType(typ) 81 | if len(sffx) == 0 { 82 | err = makeErrBadFrame("Message type %v not found", typ) 83 | return 84 | } 85 | v := strings.Split(s, " ") 86 | if len(v) != 4 && len(v) != 5 { 87 | err = makeErrBadFrame("wrong number of words (%d)", len(v)) 88 | return 89 | } 90 | 91 | front := shift(&v, 1) 92 | if front[0] != string(hof) { 93 | err = makeErrBadFrame("Bad prefix: %s (wanted %s)", front[0], string(hof)) 94 | return 95 | } 96 | 97 | expected := getStringForType(typ) 98 | tmp := pop(&v, 2) 99 | received := strings.Join(tmp, " ") 100 | if received != expected { 101 | err = makeErrBadFrame("wanted %q but got %q", expected, received) 102 | return 103 | } 104 | spfn := pop(&v, 1) 105 | if spfn[0] != strings.ToUpper(FormatName) { 106 | err = makeErrBadFrame("bad format name (%s)", spfn[0]) 107 | return 108 | } 109 | if len(v) > 0 { 110 | brand = v[0] 111 | if len(brand) > maxBrandLength { 112 | err = makeErrBadFrame("Brand is too long") 113 | return 114 | } 115 | } 116 | return brand, err 117 | } 118 | -------------------------------------------------------------------------------- /armor62_verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | ) 10 | 11 | var ( 12 | armor62SignatureHeaderChecker HeaderChecker = func(header string) (string, error) { 13 | return parseFrame(header, MessageTypeAttachedSignature, headerMarker) 14 | } 15 | armor62SignatureFrameChecker FrameChecker = func(header, footer string) (string, error) { 16 | return CheckArmor62(header, footer, MessageTypeAttachedSignature) 17 | } 18 | armor62DetachedSignatureHeaderChecker HeaderChecker = func(header string) (string, error) { 19 | return parseFrame(header, MessageTypeDetachedSignature, headerMarker) 20 | } 21 | armor62DetachedSignatureFrameChecker FrameChecker = func(header, footer string) (string, error) { 22 | return CheckArmor62(header, footer, MessageTypeDetachedSignature) 23 | } 24 | ) 25 | 26 | // NewDearmor62VerifyStream creates a stream that consumes data from reader 27 | // r. It returns the signer's public key and a reader that only 28 | // contains verified data. If the signer's key is not in keyring, 29 | // it will return an error. It expects the data it reads from r to 30 | // be armor62-encoded. 31 | func NewDearmor62VerifyStream(versionValidator VersionValidator, r io.Reader, keyring SigKeyring) (skey SigningPublicKey, vs io.Reader, brand string, err error) { 32 | dearmored, frame, err := NewArmor62DecoderStream(r, armor62SignatureHeaderChecker, armor62SignatureFrameChecker) 33 | if err != nil { 34 | return nil, nil, "", err 35 | } 36 | skey, vs, err = NewVerifyStream(versionValidator, dearmored, keyring) 37 | if err != nil { 38 | return nil, nil, "", err 39 | } 40 | if brand, err = frame.GetBrand(); err != nil { 41 | return nil, nil, "", err 42 | } 43 | return skey, vs, brand, nil 44 | } 45 | 46 | // Dearmor62Verify checks the signature in signedMsg. It returns the 47 | // signer's public key and a verified message. It expects 48 | // signedMsg to be armor62-encoded. 49 | func Dearmor62Verify(versionValidator VersionValidator, signedMsg string, keyring SigKeyring) (skey SigningPublicKey, verifiedMsg []byte, brand string, err error) { 50 | skey, stream, brand, err := NewDearmor62VerifyStream(versionValidator, bytes.NewBufferString(signedMsg), keyring) 51 | if err != nil { 52 | return nil, nil, "", err 53 | } 54 | 55 | verifiedMsg, err = io.ReadAll(stream) 56 | if err != nil { 57 | return nil, nil, "", err 58 | } 59 | 60 | return skey, verifiedMsg, brand, nil 61 | } 62 | 63 | // Dearmor62VerifyDetachedReader verifies that signature is a valid 64 | // armor62-encoded signature for entire message read from Reader, 65 | // and that the public key for the signer is in keyring. It returns 66 | // the signer's public key. 67 | func Dearmor62VerifyDetachedReader(versionValidator VersionValidator, r io.Reader, signature string, keyring SigKeyring) (skey SigningPublicKey, brand string, err error) { 68 | dearmored, brand, _, _, err := Armor62OpenWithValidation(signature, armor62DetachedSignatureHeaderChecker, armor62DetachedSignatureFrameChecker) 69 | if err != nil { 70 | return nil, "", err 71 | } 72 | skey, err = VerifyDetachedReader(versionValidator, r, dearmored, keyring) 73 | return skey, brand, err 74 | } 75 | 76 | // Dearmor62VerifyDetached verifies that signature is a valid 77 | // armor62-encoded signature for message, and that the public key 78 | // for the signer is in keyring. It returns the signer's public key. 79 | func Dearmor62VerifyDetached(versionValidator VersionValidator, message []byte, signature string, keyring SigKeyring) (skey SigningPublicKey, brand string, err error) { 80 | return Dearmor62VerifyDetachedReader(versionValidator, bytes.NewReader(message), signature, keyring) 81 | } 82 | -------------------------------------------------------------------------------- /armor62_encrypt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "io" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/keybase/saltpack/encoding/basex" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func encryptArmor62RandomData(t *testing.T, version Version, sz int) ([]byte, string) { 16 | msg := randomMsg(t, sz) 17 | err := csprngRead(msg) 18 | require.NoError(t, err) 19 | sndr := newBoxKey(t) 20 | receivers := []BoxPublicKey{newBoxKey(t).GetPublicKey()} 21 | 22 | ciphertext, err := EncryptArmor62Seal(version, msg, sndr, receivers, ourBrand) 23 | require.NoError(t, err) 24 | return msg, ciphertext 25 | } 26 | 27 | func testEncryptArmor62(t *testing.T, version Version) { 28 | plaintext, ciphertext := encryptArmor62RandomData(t, version, 1024) 29 | _, plaintext2, brand, err := Dearmor62DecryptOpen(SingleVersionValidator(version), ciphertext, kr) 30 | require.NoError(t, err) 31 | require.Equal(t, plaintext, plaintext2) 32 | brandCheck(t, brand) 33 | } 34 | 35 | func testDearmor62DecryptSlowReader(t *testing.T, version Version) { 36 | sz := 1024*16 + 3 37 | msg := randomMsg(t, sz) 38 | err := csprngRead(msg) 39 | require.NoError(t, err) 40 | sndr := newBoxKey(t) 41 | receivers := []BoxPublicKey{newBoxKey(t).GetPublicKey()} 42 | 43 | ciphertext, err := EncryptArmor62Seal(version, msg, sndr, receivers, ourBrand) 44 | require.NoError(t, err) 45 | 46 | _, dec, brand, err := NewDearmor62DecryptStream(SingleVersionValidator(version), &slowReader{[]byte(ciphertext)}, kr) 47 | require.NoError(t, err) 48 | brandCheck(t, brand) 49 | 50 | plaintext, err := io.ReadAll(dec) 51 | require.NoError(t, err) 52 | 53 | require.Equal(t, msg, plaintext) 54 | } 55 | 56 | func testNewlineInFrame(t *testing.T, version Version) { 57 | plaintext, ciphertext := encryptArmor62RandomData(t, version, 1024) 58 | 59 | // newline space space tab space 60 | ss := []string{"\n\n> ", ciphertext[0:10], "\n ", ciphertext[11:]} 61 | ciphertext = strings.Join(ss, "") 62 | 63 | _, plaintext2, brand, err := Dearmor62DecryptOpen(SingleVersionValidator(version), ciphertext, kr) 64 | require.NoError(t, err) 65 | require.Equal(t, plaintext, plaintext2) 66 | brandCheck(t, brand) 67 | } 68 | 69 | func testBadArmor62(t *testing.T, version Version) { 70 | _, ciphertext := encryptArmor62RandomData(t, version, 24) 71 | bad1 := ciphertext[0:2] + "䁕" + ciphertext[2:] 72 | _, _, _, err := Dearmor62DecryptOpen(SingleVersionValidator(version), bad1, kr) 73 | require.IsType(t, ErrBadFrame{}, err) 74 | _, _, _, err = Armor62Open(bad1) 75 | require.IsType(t, ErrBadFrame{}, err) 76 | 77 | bad2 := ciphertext[0:1] + "z" + ciphertext[2:] 78 | _, _, _, err = Dearmor62DecryptOpen(SingleVersionValidator(version), bad2, kr) 79 | require.IsType(t, ErrBadFrame{}, err) 80 | 81 | l := len(ciphertext) 82 | bad3 := ciphertext[0:(l-8)] + "z" + ciphertext[(l-7):] 83 | _, _, _, err = Dearmor62DecryptOpen(SingleVersionValidator(version), bad3, kr) 84 | requireErrContains(t, err, (ErrBadFrame{}).Error()) 85 | 86 | bad4 := ciphertext + "䁕" 87 | _, _, _, err = Dearmor62DecryptOpen(SingleVersionValidator(version), bad4, kr) 88 | requireErrSuffix(t, err, ErrTrailingGarbage.Error()) 89 | 90 | bad5 := ciphertext[0:(l-8)] + "䁕" + ciphertext[(l-7):] 91 | _, _, _, err = Armor62Open(bad5) 92 | require.IsType(t, ErrBadFrame{}, err) 93 | half := l >> 1 94 | bad6 := ciphertext[0:half] + "䁕" + ciphertext[(half+1):] 95 | _, _, _, err = Armor62Open(bad6) 96 | require.IsType(t, basex.CorruptInputError(0), err) 97 | } 98 | 99 | func TestArmor62Encrypt(t *testing.T) { 100 | tests := []func(*testing.T, Version){ 101 | testEncryptArmor62, 102 | testDearmor62DecryptSlowReader, 103 | testNewlineInFrame, 104 | testBadArmor62, 105 | } 106 | runTestsOverVersions(t, "test", tests) 107 | } 108 | -------------------------------------------------------------------------------- /encoding/basex/basex_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package basex 5 | 6 | import ( 7 | "crypto/rand" 8 | "encoding/base64" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | "golang.org/x/sync/errgroup" 13 | ) 14 | 15 | func decode(strict bool, dst, src []byte) (int, error) { 16 | if strict { 17 | return Base58StdEncodingStrict.Decode(dst, src) 18 | } 19 | return Base58StdEncoding.Decode(dst, src) 20 | } 21 | 22 | func testTestVector(t *testing.T, name string, val string, strict bool, swizzler func([]byte) []byte) { 23 | raw, err := base64.StdEncoding.DecodeString(val) 24 | if err != nil { 25 | t.Fatalf("%s: %s", name, err) 26 | } 27 | b58 := make([]byte, Base58StdEncoding.EncodedLen(len(raw))) 28 | Base58StdEncoding.Encode(b58, raw) 29 | 30 | // Potentially add spaces or random control characters or whatever else.. 31 | b58 = swizzler(b58) 32 | 33 | reenc := make([]byte, Base58StdEncoding.DecodedLen(len(b58))) 34 | n, err := decode(strict, reenc, b58) 35 | if err != nil { 36 | t.Fatalf("%s: %s", name, err) 37 | } 38 | out := base64.StdEncoding.EncodeToString(reenc[0:n]) 39 | if out != val { 40 | t.Fatalf("%s: mismatch: %s != %s", name, out, val) 41 | } 42 | } 43 | 44 | func TestVectors1(t *testing.T) { 45 | for k, v := range testEncodeVectors1 { 46 | testTestVector(t, k, v, true, func(b []byte) []byte { return b }) 47 | } 48 | } 49 | 50 | func TestVectorsSpacer(t *testing.T) { 51 | spacer := func(s []byte) []byte { 52 | var out []byte 53 | for i, c := range s { 54 | if i%5 == 0 { 55 | out = append(out, ' ') 56 | } 57 | out = append(out, c) 58 | } 59 | return out 60 | } 61 | 62 | for k, v := range testEncodeVectors1 { 63 | testTestVector(t, k, v, false, spacer) 64 | } 65 | } 66 | 67 | func testDecodeVector(t *testing.T, orig string, encoding string) { 68 | dec := make([]byte, Base58StdEncoding.DecodedLen(len(encoding))) 69 | n, err := Base58StdEncoding.Decode(dec, []byte(encoding)) 70 | if err != nil { 71 | t.Fatalf("%s: %s", orig, err) 72 | } 73 | decBase64 := base64.StdEncoding.EncodeToString(dec[0:n]) 74 | if decBase64 != orig { 75 | t.Errorf("%s != %s", decBase64, orig) 76 | } 77 | } 78 | 79 | func TestDecodeVectors(t *testing.T) { 80 | for orig, encoding := range testDecodeVectors1 { 81 | testDecodeVector(t, orig, encoding) 82 | } 83 | } 84 | 85 | func TestBadEncodings(t *testing.T) { 86 | badEncodings := []string{ 87 | "1", 88 | "B", 89 | "1111", 90 | "BBBB", 91 | "11111111", 92 | "BBBBBBBB", 93 | } 94 | var buf [100]byte 95 | for _, b := range badEncodings { 96 | n, err := Base58StdEncoding.Decode(buf[:], []byte(b)) 97 | if err == nil { 98 | t.Errorf("Should have failed to decode '%s' (got %v)", b, buf[0:n]) 99 | } 100 | } 101 | } 102 | 103 | func BenchmarkDecodeBase62(b *testing.B) { 104 | r := make([]byte, 1024*1024*16) // 16 MB of data 105 | _, err := rand.Read(r) 106 | require.NoError(b, err) 107 | data := Base62StdEncoding.EncodeToString(r) 108 | b.ResetTimer() 109 | b.SetBytes(int64(len(data))) 110 | for i := 0; i < b.N; i++ { 111 | _, err = Base62StdEncoding.DecodeString(data) 112 | require.NoError(b, err) 113 | } 114 | } 115 | 116 | func TestConcurrent(t *testing.T) { 117 | encoder := Base62StdEncodingStrict 118 | eg := errgroup.Group{} 119 | eg.Go(func() error { 120 | for i := 0; i < 2000; i++ { 121 | s := encoder.EncodeToString([]byte("testing")) 122 | _, decodeErr := encoder.DecodeString(s) 123 | if decodeErr != nil { 124 | return decodeErr 125 | } 126 | } 127 | return nil 128 | }) 129 | for i := 0; i < 2000; i++ { 130 | s := encoder.EncodeToString([]byte("testing")) 131 | _, decodeErr := encoder.DecodeString(s) 132 | if decodeErr != nil { 133 | t.Fatalf("%+v", decodeErr) 134 | } 135 | } 136 | err := eg.Wait() 137 | if err != nil { 138 | t.Fatalf("%v", err) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /armor_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "encoding/hex" 9 | "io" 10 | "os" 11 | "runtime" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func msg(sz int) []byte { 19 | res := make([]byte, sz) 20 | for i := 0; i < sz; i++ { 21 | res[i] = byte(i % 256) 22 | } 23 | return res 24 | } 25 | 26 | const ourBrand = "ACME" 27 | 28 | func brandCheck(t *testing.T, received string) { 29 | require.Equal(t, ourBrand, received) 30 | } 31 | 32 | const ( 33 | hdr = "BEGIN ACME SALTPACK ENCRYPTED MESSAGE" 34 | ftr = "END ACME SALTPACK ENCRYPTED MESSAGE" 35 | ) 36 | 37 | func testArmor(t *testing.T, sz int) { 38 | m := msg(sz) 39 | a, err := Armor62Seal(m, MessageTypeEncryption, ourBrand) 40 | require.NoError(t, err) 41 | m2, hdr2, ftr2, err := Armor62Open(a) 42 | require.NoError(t, err) 43 | require.Equal(t, m, m2) 44 | require.Equal(t, hdr, hdr2) 45 | require.Equal(t, ftr, ftr2) 46 | } 47 | 48 | func TestArmor128(t *testing.T) { 49 | testArmor(t, 128) 50 | } 51 | 52 | func TestArmor512(t *testing.T) { 53 | testArmor(t, 512) 54 | } 55 | 56 | func TestArmor1024(t *testing.T) { 57 | testArmor(t, 1024) 58 | } 59 | 60 | func TestArmor8192(t *testing.T) { 61 | testArmor(t, 8192) 62 | } 63 | 64 | func TestArmor65536(t *testing.T) { 65 | testArmor(t, 65536) 66 | } 67 | 68 | func TestSlowWriter(t *testing.T) { 69 | m := msg(1024 * 16) 70 | var out bytes.Buffer 71 | enc, err := NewArmor62EncoderStream(&out, MessageTypeEncryption, ourBrand) 72 | require.NoError(t, err) 73 | for _, c := range m { 74 | _, err = enc.Write([]byte{c}) 75 | require.NoError(t, err) 76 | } 77 | err = enc.Close() 78 | require.NoError(t, err) 79 | m2, hdr2, ftr2, err := Armor62Open(out.String()) 80 | require.NoError(t, err) 81 | require.Equal(t, m, m2) 82 | require.Equal(t, hdr, hdr2) 83 | require.Equal(t, ftr, ftr2) 84 | } 85 | 86 | type slowReader struct { 87 | buf []byte 88 | } 89 | 90 | func (sr *slowReader) Read(b []byte) (int, error) { 91 | if len(sr.buf) == 0 { 92 | return 0, io.EOF 93 | } 94 | b[0] = sr.buf[0] 95 | sr.buf = sr.buf[1:] 96 | return 1, nil 97 | } 98 | 99 | func TestSlowReader(t *testing.T) { 100 | var sr slowReader 101 | m := msg(1024 * 32) 102 | a, err := Armor62Seal(m, MessageTypeEncryption, ourBrand) 103 | require.NoError(t, err) 104 | sr.buf = []byte(a) 105 | dec, frame, err := NewArmor62DecoderStream(&sr, nil, nil) 106 | require.NoError(t, err) 107 | m2, err := io.ReadAll(dec) 108 | require.NoError(t, err) 109 | require.Equal(t, m, m2) 110 | hdr2, err := frame.GetHeader() 111 | require.NoError(t, err) 112 | require.Equal(t, hdr, hdr2) 113 | ftr2, err := frame.GetFooter() 114 | require.NoError(t, err) 115 | require.Equal(t, ftr, ftr2) 116 | } 117 | 118 | func TestBinaryInput(t *testing.T) { 119 | in, err := hex.DecodeString("96a873616c747061636b92010002c420c4afc00d50af5072094609199b54a5f8cf7b03bcea3d4945b2bbd50ac1cd42ecc41014bf77454c0b028cb009d06019981a75c4401a451af65fa3b40ae2be73b5c17dc2657992337c98ad75d4fe21de37fba2329b4970defbea176c98d306d0d285ffaa515b630224836b2c55ba1b6ba026a62102") 120 | require.NoError(t, err) 121 | 122 | done := make(chan bool) 123 | var m []byte 124 | var hdr, ftr string 125 | go func() { 126 | m, hdr, ftr, err = Armor62Open(string(in)) 127 | done <- true 128 | }() 129 | 130 | select { 131 | case <-done: 132 | case <-time.After(5 * time.Second): 133 | buf := make([]byte, 1<<16) 134 | runtime.Stack(buf, true) 135 | _, err := os.Stderr.Write(buf) 136 | require.NoError(t, err) 137 | t.Fatal("timed out waiting for Armor62Open to finish") 138 | } 139 | 140 | // Armor62Open should try to find the punctuation for the 141 | // header and hit EOF. 142 | require.Equal(t, io.ErrUnexpectedEOF, err, "Armor62Open didn't return io.ErrUnexpectedEOF: m == %v, hdr == %q, ftr == %q, err == %v", m, hdr, ftr, err) 143 | } 144 | -------------------------------------------------------------------------------- /basic/key_test.go: -------------------------------------------------------------------------------- 1 | package basic 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "reflect" 7 | "runtime" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/keybase/saltpack" 12 | ) 13 | 14 | func runTestOverVersions(t *testing.T, f func(t *testing.T, version saltpack.Version)) { 15 | for _, version := range saltpack.KnownVersions() { 16 | version := version // capture range variable. 17 | t.Run(version.String(), func(t *testing.T) { 18 | f(t, version) 19 | }) 20 | } 21 | } 22 | 23 | // runTestsOverVersions runs the given list of test functions over all 24 | // versions to test. prefix should be the common prefix for all the 25 | // test function names, and the names of the subtest will be taken to 26 | // be the strings after that prefix. Example use: 27 | // 28 | // func TestFoo(t *testing.T) { 29 | // tests := []func(*testing.T, Version){ 30 | // testFooBar1, 31 | // testFooBar2, 32 | // testFooBar3, 33 | // ... 34 | // } 35 | // runTestsOverVersions(t, "testFoo", tests) 36 | // } 37 | // 38 | // This is copied from ../common_test.go. 39 | func runTestsOverVersions(t *testing.T, prefix string, fs []func(t *testing.T, ver saltpack.Version)) { 40 | for _, f := range fs { 41 | f := f // capture range variable. 42 | name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 43 | i := strings.LastIndex(name, prefix) 44 | if i >= 0 { 45 | i += len(prefix) 46 | } else { 47 | i = 0 48 | } 49 | name = name[i:] 50 | t.Run(name, func(t *testing.T) { 51 | runTestOverVersions(t, f) 52 | }) 53 | } 54 | } 55 | 56 | func randomMsg(t *testing.T, sz int) []byte { 57 | out := make([]byte, sz) 58 | if _, err := rand.Read(out); err != nil { 59 | t.Fatal(err) 60 | } 61 | return out 62 | } 63 | 64 | func testBasicBox(t *testing.T, version saltpack.Version) { 65 | kr := NewKeyring() 66 | k1, err := kr.GenerateBoxKey() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | k2, err := kr.GenerateBoxKey() 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | msg := randomMsg(t, 1024) 75 | text, err := saltpack.EncryptArmor62Seal(version, msg, k1, []saltpack.BoxPublicKey{k2.GetPublicKey()}, "") 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | _, msg2, _, err := saltpack.Dearmor62DecryptOpen(saltpack.SingleVersionValidator(version), text, kr) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | if !bytes.Equal(msg, msg2) { 84 | t.Fatal("failed to recover message") 85 | } 86 | } 87 | 88 | func testBasicSign(t *testing.T, version saltpack.Version) { 89 | kr := NewKeyring() 90 | k1, err := kr.GenerateSigningKey() 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | msg := randomMsg(t, 1024) 95 | sig, err := saltpack.SignArmor62(version, msg, k1, "") 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | pk, msg2, _, err := saltpack.Dearmor62Verify(saltpack.SingleVersionValidator(version), sig, kr) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | if !bytes.Equal(msg, msg2) { 104 | t.Fatal("msg payload mismatch") 105 | } 106 | if !saltpack.PublicKeyEqual(k1.GetPublicKey(), pk) { 107 | t.Fatal("public signing key wasn't right") 108 | } 109 | } 110 | 111 | func TestGetRawKeys(t *testing.T) { 112 | kr := NewKeyring() 113 | k1, err := kr.GenerateSigningKey() 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | if !bytes.Equal(k1.GetRawSecretKey()[:], k1.sec[:]) { 118 | t.Fatal("signing secret key mismatch") 119 | } 120 | if !bytes.Equal(k1.GetRawPublicKey()[:], k1.pub[:]) { 121 | t.Fatal("signing public key mismatch") 122 | } 123 | k2, err := kr.GenerateBoxKey() 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | if !bytes.Equal(k2.GetRawSecretKey()[:], k2.sec[:]) { 128 | t.Fatal("box secret key mismatch") 129 | } 130 | if !bytes.Equal(k2.GetRawPublicKey()[:], k2.pub.RawBoxKey[:]) { 131 | t.Fatal("box public key mismatch") 132 | } 133 | } 134 | 135 | func TestKeyBasic(t *testing.T) { 136 | tests := []func(*testing.T, saltpack.Version){ 137 | testBasicBox, 138 | testBasicSign, 139 | } 140 | runTestsOverVersions(t, "testBasic", tests) 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # saltpack 2 | 3 | ### a modern crypto messaging format 4 | 5 | https://saltpack.org/ 6 | 7 | [![Build Status](https://github.com/keybase/saltpack/actions/workflows/ci.yml/badge.svg)](https://github.com/keybase/saltpack/actions) 8 | [![GoDoc](https://godoc.org/github.com/keybase/saltpack?status.svg)](https://godoc.org/github.com/keybase/saltpack) 9 | 10 | **saltpack** is a streamlined, modern solution, designed with simplicity in mind. It is easy to implement & integrate. We've made few crypto decisions and instead leave almost all of the heavy lifting to the [NaCl library](https://godoc.org/golang.org/x/crypto/nacl). 11 | 12 | **saltpack** is a binary message format, encoded using the [MessagePack](http://msgpack.org/) format. Messages are broken up into reasonable (1MB) chunks, over which regular [NaCl operations](https://nacl.cr.yp.to/) are performed. We have taken pains to address many of the [shortcomings](https://www.imperialviolet.org/2015/05/16/aeads.html) of current message formats: (1) only authenticated data is output; (2) repudiable authentication is used wherever possible; (3) chunks cannot be reordered or combined with other transmissions; (4) the public keys of senders and recipients can be hidden; and (5) message truncation is detectable. 13 | 14 | Visually speaking, a **saltpack** ASCII output looks a lot like PGP's. 15 | 16 | ### saltpack 17 | 18 | ``` 19 | BEGIN SALTPACK SIGNED MESSAGE. kXR7VktZdyH7rvq v5wcIkHbs7XwHpb 20 | nPtLcF6vE5yY63t aHF62jiEC1zHGqD inx5YqK0nf5W9Lp TvUmM2zBwxgd3Nw 21 | kvzZ96W7ZfDdTVg F5Y99c2l5EsCy1I xVNl0nY1TP25vsX 2cRXXPUrM2UKtWq 22 | UK2HG2ifBSOED4w xArcORHfFeiEZxF CqestMqLSCCE6lT HFcdvt1QX9JjmWL 23 | o5AAqPiECnoHiSA bPHhz2JnSCyDIOz ZET1BWzttbMDL4N pcyQLmsGqYpxhG6 24 | uvdBxdt55w9xQvQ hDPuOsKF05Hsml6 z7h9TS2msJcNwtz vxGIQR7sbB19UOt 25 | boM1hlolmMB3loP 0KexlROFBTDC6MR nBvd9sZUxA8Z7i5 a6Dk5yFU3WEYQAo 26 | DqqjXcp0yBoHO5O KEMqkZlyMf1PKiB 2n9wE6jwxAN1xws ccthT6X3iRYk0Br 27 | gHW6QRXzAHLy6Ib LgY6b3UcQAoDo8b XyaExxinVuM5Ftk 75BJOWoyLGFhZS7 28 | EfKR8jQQexvyjDM rJLxYtjvaLX7joS 2q1VcUlqGfZDhAa 4vxJQAyu57beOux 29 | oobLhI47iZf9bxK PmYrVQ5PsC6pY1J KTQQexvlvp2yicx K4su2AFCjihbzNI 30 | yZgKM4NHN1KZapS O3iB9SlhVfTfFcR FoQoSViTkbtDtTt 6I0jrTRHkv9XVQQ 31 | eeeuzR7qYu1Grm3 zDPyj7JgK2mDidw HchOZnfOn59QLnM nH7ErnPRXgHuWHG 32 | DBidjQPakJHuWsk 2ftpIyZd2NLYEFS Mqcbo6QeCdk7LA1 uobl4NXzpvi8amO 33 | Pe8xAl1OzUCoD34 MbCwtTAe1JNymvs okufV8lHU0jVnbj u4no9QB9aP2Wkjx 34 | PfeqIH2fEtOjmFP gPMhGWslkU0M7FL QP77gPHbgjPLSD8 yIRTrbgzpAPut5R 35 | QhIdqVlHbUOa9sI v7gSqOi0GbUlhSM 183LxZI8pIlvgn9 Ms1WNzt5Xkv0W1Q 36 | Qf419ZmuQVPQDOk 0hffDmUk71TlfVx XZCF3voC2ysgl3g YdLz4rDRzMJgd2m 37 | 01HIbfdsoZpAMty O27WtUNRLV1iyC9 tK5ApCyekI4nWcf 2OvTHnC8ma7bloW 38 | XAG. END SALTPACK SIGNED MESSAGE. 39 | ``` 40 | 41 | ### PGP 42 | 43 | ``` 44 | -----BEGIN PGP MESSAGE----- 45 | Comment: GPGTools - https://gpgtools.org 46 | 47 | owEBUAKv/ZANAwAKAdIkQTsc+mSQAcsgYgBWZ0C0SSBhbSBzbyBzaWNrIG9mIHRo 48 | aXMgc2hpdAqJAhwEAAEKAAYFAlZnQLQACgkQ0iRBOxz6ZJBS9Q/+MSfWiOz5OvRt 49 | lHTncX8Ifo7+wSKYH039vEQAUvj+rnEdlBzcJPoHDE1yZxAZT5ek5S+cxQ5bx55K 50 | WRLvw/sAz+OU0OPHSDsqI2LjU6D+s1EvwCISkXoWlMVx5vJsEz2XGlQ8DzgBC2Jy 51 | wPanQf1lUz0c7k0ySdCTdZ0qG1YuaYnCXsS6g/E8E7TIO++2v5EbkgYZl3Io2LcI 52 | C9TqTHdrIc7WGTSFjwq9JIgvwfuShpccNSFQ262gSJh8rUOzzY37q81pKxDnBvEV 53 | TMrQYY0e/JK7KMMcHDSQSeWnMxf4/v5Qex7WI55CW4++qbNvDylDi9fTpkYfXl3B 54 | L8pbBAxMUjcJX4qVVzWcxTwSXYO29Bi4osn2klNyZHnO35kuI9XGziWCGqhVx1MW 55 | ptNHoVjk7/Uo7k39hY0Vjltnl/SqXHq/H7YTRSgLebuhn6zqMbmFXtyHYSHGgAQ4 56 | rcdSBta+I9tmYCnp1GmfeXff2wzsFYPUune2Hve4VghjmeU0x7OWMEl93gpznSwu 57 | NvzyOCqFCyfEmt/R2QCXAkxwPU/Mdsd5vzEHSMkcZgW4CTr+j5YG/C3kMy7UJAGZ 58 | ZzFAh3/Z8fCtfREF3zH48XbNh3dQXNl40bUF/AgPvLqPf35L7TCchcUAC7oiASa/ 59 | Ph/Hao4ZzCQDM76Jr/aCUJIbxyc2zco= 60 | =eyef 61 | -----END PGP MESSAGE----- 62 | ``` 63 | 64 | The changes here are small: we've reduced our characters to base62 plus some period markers, and only at the ends of words. PGP messages often get mangled by different apps, websites, and smart text processors. 65 | 66 | Of course, **saltpack** can output binary, too. Either way, it's what's inside the format that matters. You can read the [spec](https://saltpack.org/encryption-format-v2) for the details. 67 | 68 | Post issues to: https://github.com/keybase/keybase-issues 69 | -------------------------------------------------------------------------------- /armor62.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "io" 8 | 9 | "github.com/keybase/saltpack/encoding/basex" 10 | ) 11 | 12 | // Armor62Params are the armoring parameters we recommend for use with 13 | // a generic armorer. It specifies the spaces between words, the spacing 14 | // between lines, some simple punctuation, and an encoding alphabet. 15 | var Armor62Params = armorParams{ 16 | BytesPerWord: 15, 17 | WordsPerLine: 200, 18 | Punctuation: byte('.'), 19 | Encoding: basex.Base62StdEncoding, 20 | } 21 | 22 | // NewArmor62EncoderStream makes a new Armor 62 encoding stream, using the base62-alphabet 23 | // and a 32/43 encoding rate strategy. Pass it an `encoded` stream writer to write the 24 | // encoded stream to. Also pass an optional "brand" . It will 25 | // return an io.WriteCloser on success, that you can write raw (unencoded) data to. 26 | // An error will be returned if there is trouble writing the header to encoded. 27 | // 28 | // To make the output look pretty, a space is inserted every 15 characters of output, 29 | // and a newline is inserted every 200 words. 30 | func NewArmor62EncoderStream(encoded io.Writer, typ MessageType, brand string) (io.WriteCloser, error) { 31 | hdr := makeFrame(headerMarker, typ, brand) 32 | ftr := makeFrame(footerMarker, typ, brand) 33 | return newArmorEncoderStream(encoded, hdr, ftr, Armor62Params) 34 | } 35 | 36 | // Armor62Seal takes an input plaintext and returns and output armor encoding 37 | // as a string, or an error if a problem was encountered. Also provide a header 38 | // and a footer to frame the message. Uses Base62 encoding scheme 39 | func Armor62Seal(plaintext []byte, typ MessageType, brand string) (string, error) { 40 | hdr := makeFrame(headerMarker, typ, brand) 41 | ftr := makeFrame(footerMarker, typ, brand) 42 | return armorSeal(plaintext, hdr, ftr, Armor62Params) 43 | } 44 | 45 | // NewArmor62DecoderStream is used to decode input base62-armoring format. It returns 46 | // a stream you can read from, and also a Frame you can query to see what the open/close 47 | // frame markers were. hc and fc are optional and can be nil. 48 | func NewArmor62DecoderStream(r io.Reader, hc HeaderChecker, fc FrameChecker) (io.Reader, Frame, error) { 49 | return newArmorDecoderStream(r, Armor62Params, hc, fc) 50 | } 51 | 52 | // Armor62Open runs armor stream decoding, but on a string, and it outputs 53 | // a string. It does not do any validation on the header and footer. 54 | // 55 | // Deprecated: use Armor62OpenWithValidation instead. 56 | func Armor62Open(msg string) (body []byte, header string, footer string, err error) { 57 | body, _, header, footer, err = Armor62OpenWithValidation(msg, nil, nil) 58 | return body, header, footer, err 59 | } 60 | 61 | // Armor62OpenWithValidation runs armor stream decoding, but on a string, and it outputs 62 | // a string. It validates header and footer with the provided checkers (which are optional and can be nil). 63 | func Armor62OpenWithValidation(msg string, hc HeaderChecker, fc FrameChecker) (body []byte, brand string, header string, footer string, err error) { 64 | return armorOpen(msg, Armor62Params, hc, fc) 65 | } 66 | 67 | // CheckArmor62Frame checks that the frame matches our standard 68 | // begin/end frame 69 | func CheckArmor62Frame(frame Frame, typ MessageType) (brand string, err error) { 70 | var hdr, ftr string 71 | if hdr, err = frame.GetHeader(); err != nil { 72 | return "", err 73 | } 74 | if ftr, err = frame.GetFooter(); err != nil { 75 | return "", err 76 | } 77 | return CheckArmor62(hdr, ftr, typ) 78 | } 79 | 80 | // CheckArmor62 checks that the frame matches our standard 81 | // begin/end frame 82 | func CheckArmor62(hdr string, ftr string, typ MessageType) (brand string, err error) { 83 | brand, err = parseFrame(hdr, typ, headerMarker) 84 | if err != nil { 85 | return "", err 86 | } 87 | var b2 string 88 | b2, err = parseFrame(ftr, typ, footerMarker) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | if b2 != brand { 94 | return "", makeErrBadFrame("brand mismatch: %q != %q", brand, b2) 95 | } 96 | return brand, nil 97 | } 98 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | // MessageType is an int used to describe what "type" of message it is. 7 | type MessageType int 8 | 9 | // packetSeqno is a special int type used to describe which packet in the 10 | // sequence we're dealing with. The header is always at seqno=0. Other packets 11 | // follow. Note that there is a distinction between packetSeqno and encryptionBlockNumber. 12 | // In general, the former is one more than the latter. 13 | type packetSeqno uint64 14 | 15 | // MessageTypeUnknown is used by the decoding functions 16 | // to indicate an unknown message type or a decoding error. 17 | // This is NOT a constant in the saltpack spec, and is specific 18 | // to this library implementation. 19 | const MessageTypeUnknown MessageType = -1 20 | 21 | // MessageTypeEncryption is a packet type to describe an 22 | // encryption message. 23 | const MessageTypeEncryption MessageType = 0 24 | 25 | // MessageTypeAttachedSignature is a packet type to describe an 26 | // attached signature. 27 | const MessageTypeAttachedSignature MessageType = 1 28 | 29 | // MessageTypeDetachedSignature is a packet type to describe a 30 | // detached signature. 31 | const MessageTypeDetachedSignature MessageType = 2 32 | 33 | // MessageTypeSigncryption is a packet type to describe a 34 | // signcrypted message. 35 | const MessageTypeSigncryption MessageType = 3 36 | 37 | // Version1 returns the Version for Saltpack V1. 38 | func Version1() Version { 39 | return Version{Major: 1, Minor: 0} 40 | } 41 | 42 | // Version2 returns the Version for Saltpack V2. 43 | func Version2() Version { 44 | return Version{Major: 2, Minor: 0} 45 | } 46 | 47 | // CurrentVersion returns the Version for the currently-used Saltpack 48 | // version. 49 | func CurrentVersion() Version { 50 | return Version2() 51 | } 52 | 53 | // KnownVersions returns all known Saltpack versions. 54 | func KnownVersions() []Version { 55 | return []Version{Version1(), Version2()} 56 | } 57 | 58 | // encryptionBlockSize is by default 1MB and can't currently be tweaked. 59 | const encryptionBlockSize int = 1048576 60 | 61 | // EncryptionArmorString is included in armor headers for encrypted messages. 62 | const EncryptionArmorString = "ENCRYPTED MESSAGE" 63 | 64 | // SignedArmorString is included in armor headers for signed messages 65 | const SignedArmorString = "SIGNED MESSAGE" 66 | 67 | // DetachedSignatureArmorString is included in armor headers for detached signatures. 68 | const DetachedSignatureArmorString = "DETACHED SIGNATURE" 69 | 70 | // FormatName is the publicly advertised name of the format, used in 71 | // the header of the message and also in Nonce creation. 72 | const FormatName = "saltpack" 73 | 74 | // signatureBlockSize is by default 1MB and can't currently be tweaked. 75 | const signatureBlockSize int = 1048576 76 | 77 | // signatureAttachedString is part of the data that is signed in 78 | // each payload packet. 79 | const signatureAttachedString = "saltpack attached signature\x00" 80 | 81 | // signatureDetachedString is part of the data that is signed in 82 | // a detached signature. 83 | const signatureDetachedString = "saltpack detached signature\x00" 84 | 85 | // signatureEncryptedString is part of the data that is signed in 86 | // a signcryption signature. 87 | const signatureEncryptedString = "saltpack encrypted signature\x00" 88 | 89 | // signcryptionDerivedSymmetricKeyContext gets mixed in with the long term symmetric 90 | // key and ephemeral key inputs, as an HMAC key 91 | const signcryptionSymmetricKeyContext = "saltpack signcryption derived symmetric key" 92 | 93 | // signcryptionBoxKeyIdentifierContext gets mixed in with the DH shared secret 94 | // as an HMAC key, to make an opaque identifier 95 | const signcryptionBoxKeyIdentifierContext = "saltpack signcryption box key identifier" 96 | 97 | // We truncate HMAC512 to the same link that NaCl's crypto_auth function does. 98 | const cryptoAuthBytes = 32 99 | 100 | const cryptoAuthKeyBytes = 32 101 | 102 | func (m MessageType) String() string { 103 | switch m { 104 | case MessageTypeEncryption: 105 | return "an encrypted message" 106 | case MessageTypeDetachedSignature: 107 | return "a detached signature" 108 | case MessageTypeAttachedSignature: 109 | return "an attached signature" 110 | case MessageTypeSigncryption: 111 | return "a signed and encrypted message" 112 | default: 113 | return "an unknown message type" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /punctuated_reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "io" 10 | ) 11 | 12 | // punctuatedReader is a stream reader that reads until it hits a usual 13 | // error OR until it hits a punctuation character. In that latter case 14 | // it returns an `ErrPunctuation` "error" so that way callers can tell 15 | // the difference between a normal EOF and a "punctuated" EOF. 16 | type punctuatedReader struct { 17 | r io.Reader 18 | punctuation [1]byte 19 | nextSegment []byte 20 | thisSegment []byte 21 | errThisSegment error 22 | buf [4096]byte 23 | } 24 | 25 | // ErrPunctuated is produced when a punctuation character is found in the stream. 26 | // It can be returned along with data, unlike usual errors. 27 | var ErrPunctuated = errors.New("found punctuation in stream") 28 | 29 | // ErrOverflow is returned if we were looking for punctuation but our quota was 30 | // overflowed before we found the needed character. 31 | var ErrOverflow = errors.New("buffer was overflowed before we found punctuation") 32 | 33 | // Read from the punctuatedReader, potentially returning an `ErrPunctuation` 34 | // if a punctuation character was found. 35 | func (p *punctuatedReader) Read(out []byte) (n int, err error) { 36 | // First deal with the case that we had a "short copy" to our target buffer 37 | // in a previous call of the read function. 38 | if len(p.thisSegment) > 0 { 39 | n = copy(out, p.thisSegment) 40 | p.thisSegment = p.thisSegment[n:] 41 | if len(p.thisSegment) == 0 { 42 | err = p.errThisSegment 43 | p.errThisSegment = nil 44 | } 45 | return n, err 46 | } 47 | 48 | // In this case we have no previous short copy, so now we deal with 49 | // two cases --- we had, from a previous iteration, some stuff after the 50 | // punctuation. Or we need to read again. 51 | var src []byte 52 | usedBuffer := false 53 | if len(p.nextSegment) > 0 { 54 | src = p.nextSegment 55 | usedBuffer = true 56 | p.nextSegment = nil 57 | } else { 58 | n, err = p.r.Read(out) 59 | if err != nil { 60 | return n, err 61 | } 62 | src = out[0:n] 63 | } 64 | 65 | // Now look for punctuation. If we find it, we need to keep the remaining 66 | // data in the buffer (to the right of the punctuation mark) for the next 67 | // time through the loop. Note that that new buffer can itself have subsequent 68 | // punctuation, so we'll have to perform the check again here. 69 | foundPunc := false 70 | if i := bytes.Index(src, p.punctuation[:]); i >= 0 { 71 | p.nextSegment = src[(i + 1):] 72 | src = src[0:i] 73 | n = len(src) 74 | foundPunc = true 75 | } 76 | 77 | // If we used a buffer, copy into the output buffer, and potentially deal 78 | // with a "short copy" situation in which we couldn't fit all of the data 79 | // into the given buffer. 80 | if usedBuffer { 81 | n = copy(out, src) 82 | p.thisSegment = src[n:] 83 | } 84 | 85 | // If we found punctuation, we have to set an error accordingly. However, 86 | // in the "short copy" situation just above, we can't return the error just 87 | // yet, we need to do so when that buffer is drained. 88 | if foundPunc { 89 | if len(p.thisSegment) > 0 { 90 | p.errThisSegment = ErrPunctuated 91 | } else { 92 | err = ErrPunctuated 93 | } 94 | } 95 | 96 | return n, err 97 | } 98 | 99 | // ReadUntilPunctuation reads from the stream until it find a desired 100 | // punctuation byte. If it wasn't found before EOF, it will return io.ErrUnexpectedEOF. 101 | // If it wasn't found before lim bytes are consumed, then it will return ErrOverflow. 102 | func (p *punctuatedReader) ReadUntilPunctuation(lim int) (res []byte, err error) { 103 | for { 104 | var n int 105 | n, err = p.Read(p.buf[:]) 106 | switch err { 107 | case nil, ErrPunctuated: 108 | res = append(res, p.buf[0:n]...) 109 | if err == ErrPunctuated { 110 | err = nil 111 | return res, err 112 | } 113 | if len(res) >= lim { 114 | return nil, ErrOverflow 115 | } 116 | case io.EOF: 117 | err = io.ErrUnexpectedEOF 118 | fallthrough 119 | default: 120 | return nil, err 121 | } 122 | 123 | if n == 0 { 124 | return nil, io.ErrUnexpectedEOF 125 | } 126 | } 127 | } 128 | 129 | // newPunctuatedReader returns a new punctuatedReader given an underlying 130 | // read stream and a punctuation byte. 131 | func newPunctuatedReader(r io.Reader, p byte) *punctuatedReader { 132 | ret := &punctuatedReader{r: r} 133 | ret.punctuation[0] = p 134 | return ret 135 | } 136 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2009 The Go Authors. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // * Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // * Redistributions in binary form must reproduce the above 10 | // copyright notice, this list of conditions and the following disclaimer 11 | // in the documentation and/or other materials provided with the 12 | // distribution. 13 | // * Neither the name of Google Inc. nor the names of its 14 | // contributors may be used to endorse or promote products derived from 15 | // this software without specific prior written permission. 16 | // 17 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 30 | // this source code is governed by the included BSD license. 31 | 32 | // (Following the advice of 33 | // https://softwareengineering.stackexchange.com/a/264363 re. above 34 | // copyright notices.) 35 | 36 | package saltpack 37 | 38 | import ( 39 | cryptorand "crypto/rand" 40 | "encoding/binary" 41 | "io" 42 | ) 43 | 44 | // csprngReadFull is a thin wrapper around io.ReadFull on a given 45 | // CSPRNG that also (paranoidly) checks the length. 46 | func csprngReadFull(csprng io.Reader, b []byte) error { 47 | n, err := io.ReadFull(csprng, b) 48 | if err != nil { 49 | return err 50 | } 51 | if n != len(b) { 52 | return ErrInsufficientRandomness 53 | } 54 | return nil 55 | } 56 | 57 | // csprngRead is like crypto/rand.Read, except it uses csprngReadFull 58 | // instead of io.ReadFull. 59 | func csprngRead(b []byte) error { 60 | return csprngReadFull(cryptorand.Reader, b) 61 | } 62 | 63 | // csprngUint32, given a CSPRNG, returns a uniformly distributed 64 | // random number in [0, 2³²). 65 | func csprngUint32(csprng io.Reader) (uint32, error) { 66 | var buf [4]byte 67 | err := csprngReadFull(csprng, buf[:]) 68 | if err != nil { 69 | return 0, err 70 | } 71 | 72 | return binary.BigEndian.Uint32(buf[:]), nil 73 | } 74 | 75 | // csprngUint32n, given a CSPRNG, returns, as a uint32, a uniformly 76 | // distributed random number in [0, n). It is adapted from 77 | // math/rand.int31n from go 1.10. 78 | // 79 | // For implementation details, see: 80 | // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction 81 | // https://lemire.me/blog/2016/06/30/fast-random-shuffling 82 | func csprngUint32n(csprng io.Reader, n uint32) (uint32, error) { 83 | v, err := csprngUint32(csprng) 84 | if err != nil { 85 | return 0, err 86 | } 87 | prod := uint64(v) * uint64(n) 88 | //nolint:gosec // intentionally taking low 32 bits 89 | low := uint32(prod) 90 | if low < n { 91 | thresh := -n % n 92 | for low < thresh { 93 | v, err = csprngUint32(csprng) 94 | if err != nil { 95 | return 0, err 96 | } 97 | prod = uint64(v) * uint64(n) 98 | //nolint:gosec // intentionally taking low 32 bits 99 | low = uint32(prod) 100 | } 101 | } 102 | //nolint:gosec // intentionally taking high 32 bits 103 | return uint32(prod >> 32), nil 104 | } 105 | 106 | // csprngShuffle randomizes the order of elements given a CSPRNG. n is 107 | // the number of elements, which must be >= 0 and < 2³¹. swap swaps 108 | // the elements with indexes i and j. 109 | // 110 | // This function implements 111 | // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle , and is 112 | // adapted from math/rand.Shuffle from go 1.10. 113 | func csprngShuffle(csprng io.Reader, n int, swap func(i, j int)) error { 114 | if n < 0 { 115 | panic("csprngShuffle: n < 0") 116 | } 117 | if n > ((1 << 31) - 1) { 118 | panic("csprngShuffle: n >= 2³¹") 119 | } 120 | 121 | for i := n - 1; i > 0; i-- { 122 | //nolint:gosec // i+1 is bounded by n < 2³¹, conversion is safe 123 | j, err := csprngUint32n(csprng, uint32(i+1)) 124 | if err != nil { 125 | return err 126 | } 127 | swap(i, int(j)) 128 | } 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /decrypt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "io" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestDecryptVersionValidator(t *testing.T) { 16 | plaintext := []byte{0x01} 17 | sender := newBoxKey(t) 18 | receivers := []BoxPublicKey{newBoxKey(t).GetPublicKey()} 19 | ciphertext, err := Seal(Version1(), plaintext, sender, receivers) 20 | require.NoError(t, err) 21 | 22 | _, _, err = Open(SingleVersionValidator(Version2()), ciphertext, kr) 23 | expectedErr := ErrBadVersion{Version1()} 24 | require.Equal(t, expectedErr, err) 25 | } 26 | 27 | func testDecryptNewMinorVersion(t *testing.T, version Version) { 28 | plaintext := []byte{0x01} 29 | 30 | newVersion := version 31 | newVersion.Minor++ 32 | 33 | teo := testEncryptionOptions{ 34 | corruptHeader: func(eh *EncryptionHeader) { 35 | eh.Version = newVersion 36 | }, 37 | } 38 | sender := newBoxKey(t) 39 | receivers := []BoxPublicKey{newBoxKey(t).GetPublicKey()} 40 | ciphertext, err := testSeal(version, plaintext, sender, receivers, teo) 41 | require.NoError(t, err) 42 | 43 | _, _, err = Open(SingleVersionValidator(newVersion), ciphertext, kr) 44 | require.NoError(t, err) 45 | } 46 | 47 | type errAtEOFReader struct { 48 | io.Reader 49 | errAtEOF error 50 | } 51 | 52 | func (r errAtEOFReader) Read(p []byte) (n int, err error) { 53 | n, err = r.Reader.Read(p) 54 | if err == io.EOF { 55 | err = r.errAtEOF 56 | } 57 | return n, err 58 | } 59 | 60 | func testDecryptErrorAtEOF(t *testing.T, version Version) { 61 | plaintext := randomMsg(t, 128) 62 | sender := newBoxKey(t) 63 | receivers := []BoxPublicKey{newBoxKey(t).GetPublicKey()} 64 | ciphertext, err := Seal(version, plaintext, sender, receivers) 65 | require.NoError(t, err) 66 | 67 | var reader io.Reader = bytes.NewReader(ciphertext) 68 | errAtEOF := errors.New("err at EOF") 69 | reader = errAtEOFReader{reader, errAtEOF} 70 | _, stream, err := NewDecryptStream(SingleVersionValidator(version), reader, kr) 71 | require.NoError(t, err) 72 | 73 | msg, err := io.ReadAll(stream) 74 | requireErrSuffix(t, err, errAtEOF.Error()) 75 | 76 | // Since the bytes are still authenticated, the decrypted 77 | // message should still compare equal to the original input. 78 | require.Equal(t, plaintext, msg) 79 | } 80 | 81 | func TestDecrypt(t *testing.T) { 82 | tests := []func(*testing.T, Version){ 83 | testDecryptNewMinorVersion, 84 | testDecryptErrorAtEOF, 85 | } 86 | runTestsOverVersions(t, "testDecrypt", tests) 87 | } 88 | 89 | const hardcodedV1EncryptedMessage = ` 90 | BEGIN KEYBASE SALTPACK ENCRYPTED MESSAGE. kiPgBwdlv6bV9N8 dSkCbjKrku5ZO7I 91 | sQfGHBd7ZxroT7P 1oooGf4WjNkflSq ujGii7s89UFEybr MCxPEHJ7oOvWtnu Hos4mnLWEggEbcO 92 | 1799w2eUijCv0AO E4GK7kPKPSFiF5m enAE17GVaRn34Vv wlwxB9LgFzNfg4m D03qjZnVIeBstvT 93 | TGBDN7BnaSiUjW4 Ao0VbJmjuwI2gqt BqTefCIubT0ZvxO zFN8PAoclVLLbWf pPgjOB7eVp3Bbnq 94 | 6nhA8Ql55rMNEx8 9XOTpJh4yJBzA5E rpiLelEIo0LfHMA 4WEI2Lk1FXF3txw LPSWpzStekiIImR 95 | tY2Uhf7hcRZFs1P yRr4WYFoWpjotGA 2k6S0L8QHGPbsGl jJKz5m1at0o8XxA MrWrtBnOmkK1kgS 96 | TNm9UX5DiaVxyJ8 4JKgJVTt8JxMacq 37vn4jogmZJr45r gNSrakw8sFv8CaD xMNXqUWkhQ9U8ZI 97 | N1ePua5gTPaECSD ZonBMFRUDpHBFHQ z7hhFmOww4qkUXm xQdpNDg9Ex7YvRT 0CPvP9FsEelrNFH 98 | 4xiDSnDAYMguoC6 yC5YmGrYxusmfWC 7CAMYK0lQuuIucF aZCvYRTGRjDj0BA 8vvlXPHcjkyE956 99 | RPY6fYiwVBf2dZg 8lRgd4NjOHdz6v9 6vt3nHGx4ZiUUNT 70xwTjNVIVbH5kV UTI0igySEhyh49z 100 | X5rcwPdcuA2zO4d nyrYEqrAT55ZPsp stRGwbHgQRm36wD c06Z4xYUJv5AtUr R02MT9AqytNeLvu 101 | KvYolx5Wlm95FtR k6EaQ0hfC4oS1nF 6qRgICgl4JaSLBi baciijBMud23IJg aOHE9dR9ZnGJsLm 102 | tgDdKRzle5KLksB sSZiiGKf5uAFr9A Tx9JhFZv3B9GP5v 2s3U289T97Y0hhS UEcuMcyDSbyOLko 103 | dSbguBO4iKLGL6A T1lPhaCzg4n4vZv wW3qEKEflxsRu8O GoS5bg3586PGYP6 UlTCS6uZDZDvZpa 104 | FuHsCazBwbC8RMw mK04rfrmwew. END KEYBASE SALTPACK ENCRYPTED MESSAGE. 105 | ` 106 | const hardcodedV1DecryptionKey = "1fcf32dbefa43c1af55f1387b5e30117657a6eb9ef1bbbd4e95b3f1436fc3310" 107 | 108 | func requireDearmor62DecryptOpenTo(t *testing.T, expectedPlaintext string, version Version, secretKeyString secretKeyString, armoredCiphertext string) { 109 | key, err := secretKeyString.toSecretKey() 110 | require.NoError(t, err) 111 | keyring := newKeyring() 112 | keyring.insert(key) 113 | _, plaintext, _, err := Dearmor62DecryptOpen(SingleVersionValidator(version), armoredCiphertext, keyring) 114 | require.NoError(t, err) 115 | require.Equal(t, expectedPlaintext, string(plaintext)) 116 | } 117 | 118 | func TestHardcodedEncryptedMessageV1(t *testing.T) { 119 | requireDearmor62DecryptOpenTo(t, "test message!", Version1(), hardcodedV1DecryptionKey, hardcodedV1EncryptedMessage) 120 | } 121 | 122 | func testEncryptArmor62SealResultOpen(t *testing.T, result encryptArmor62SealResult) { 123 | for _, receiver := range result.receivers { 124 | requireDearmor62DecryptOpenTo(t, result.plaintext, result.version, receiver, result.armoredCiphertext) 125 | } 126 | } 127 | 128 | func TestOpenHardcodedEncryptMessageV1(t *testing.T) { 129 | testEncryptArmor62SealResultOpen(t, v1EncryptArmor62SealResult) 130 | } 131 | 132 | func TestOpenHardcodedEncryptMessageV2(t *testing.T) { 133 | testEncryptArmor62SealResultOpen(t, v2EncryptArmor62SealResult) 134 | } 135 | -------------------------------------------------------------------------------- /armor62_signcrypt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | ) 10 | 11 | var ( 12 | // Signcryption has the same frame as encryption 13 | armor62SigncryptionHeaderChecker = armor62EncryptionHeaderChecker 14 | armor62SigncryptionFrameChecker = armor62EncryptionFrameChecker 15 | ) 16 | 17 | func newSigncryptArmor62SealStream(ciphertext io.Writer, sender SigningSecretKey, receiverBoxKeys []BoxPublicKey, receiverSymmetricKeys []ReceiverSymmetricKey, ephemeralKeyCreator EphemeralKeyCreator, rng signcryptRNG, brand string) (plaintext io.WriteCloser, err error) { 18 | // Note: same "BEGIN SALTPACK ENCRYPTED" visible message type. 19 | enc, err := NewArmor62EncoderStream(ciphertext, MessageTypeEncryption, brand) 20 | if err != nil { 21 | return nil, err 22 | } 23 | out, err := newSigncryptSealStream(enc, sender, receiverBoxKeys, receiverSymmetricKeys, ephemeralKeyCreator, rng) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return closeForwarder([]io.WriteCloser{out, enc}), nil 28 | } 29 | 30 | // NewSigncryptArmor62SealStream creates a stream that consumes plaintext data. 31 | // It will write out signcrypted data to the io.Writer passed in as ciphertext. 32 | // The signcryption is from the specified sender, and is signcrypted for the 33 | // given receivers. 34 | // 35 | // The "brand" is the optional "brand" string to put into the header 36 | // and footer. 37 | // 38 | // The ciphertext is additionally armored with the recommended armor62-style format. 39 | // 40 | // If initialization succeeds, returns an io.WriteCloser that accepts 41 | // plaintext data to be signcrypted and a nil error. Otherwise, 42 | // returns nil and the initialization error. 43 | // 44 | // ephemeralKeyCreator should be the last argument; it's the 2nd one 45 | // to preserve the public API. 46 | func NewSigncryptArmor62SealStream(ciphertext io.Writer, ephemeralKeyCreator EphemeralKeyCreator, sender SigningSecretKey, receiverBoxKeys []BoxPublicKey, receiverSymmetricKeys []ReceiverSymmetricKey, brand string) (plaintext io.WriteCloser, err error) { 47 | return newSigncryptArmor62SealStream(ciphertext, sender, receiverBoxKeys, receiverSymmetricKeys, ephemeralKeyCreator, defaultSigncryptRNG{}, brand) 48 | } 49 | 50 | func signcryptArmor62Seal(plaintext []byte, sender SigningSecretKey, receiverBoxKeys []BoxPublicKey, receiverSymmetricKeys []ReceiverSymmetricKey, ephemeralKeyCreator EphemeralKeyCreator, rng signcryptRNG, brand string) (string, error) { 51 | var buf bytes.Buffer 52 | enc, err := newSigncryptArmor62SealStream(&buf, sender, receiverBoxKeys, receiverSymmetricKeys, ephemeralKeyCreator, rng, brand) 53 | if err != nil { 54 | return "", err 55 | } 56 | if _, err := enc.Write(plaintext); err != nil { 57 | return "", err 58 | } 59 | if err := enc.Close(); err != nil { 60 | return "", err 61 | } 62 | return buf.String(), nil 63 | } 64 | 65 | // SigncryptArmor62Seal is the non-streaming version of NewSigncryptArmor62SealStream, which 66 | // inputs a plaintext (in bytes) and output a ciphertext (as a string). 67 | // 68 | // ephemeralKeyCreator should be the last argument; it's the 2nd one 69 | // to preserve the public API. 70 | func SigncryptArmor62Seal(plaintext []byte, ephemeralKeyCreator EphemeralKeyCreator, sender SigningSecretKey, receiverBoxKeys []BoxPublicKey, receiverSymmetricKeys []ReceiverSymmetricKey, brand string) (string, error) { 71 | return signcryptArmor62Seal(plaintext, sender, receiverBoxKeys, receiverSymmetricKeys, ephemeralKeyCreator, defaultSigncryptRNG{}, brand) 72 | } 73 | 74 | // NewDearmor62SigncryptOpenStream makes a new stream that dearmors and decrypts the given 75 | // Reader stream. Pass it a keyring so that it can lookup private and public keys 76 | // as necessary. Returns the MessageKeyInfo recovered during header 77 | // processing, an io.Reader stream from which you can read the plaintext, the armor branding, and 78 | // maybe an error if there was a failure. 79 | func NewDearmor62SigncryptOpenStream(ciphertext io.Reader, keyring SigncryptKeyring, resolver SymmetricKeyResolver) (SigningPublicKey, io.Reader, string, error) { 80 | dearmored, frame, err := NewArmor62DecoderStream(ciphertext, armor62SigncryptionHeaderChecker, armor62SigncryptionFrameChecker) 81 | if err != nil { 82 | return nil, nil, "", err 83 | } 84 | brand, err := frame.GetBrand() 85 | if err != nil { 86 | return nil, nil, "", err 87 | } 88 | mki, r, err := NewSigncryptOpenStream(dearmored, keyring, resolver) 89 | if err != nil { 90 | return mki, nil, "", err 91 | } 92 | return mki, r, brand, nil 93 | } 94 | 95 | // Dearmor62SigncryptOpen takes an armor62'ed, encrypted ciphertext and attempts to 96 | // dearmor and decrypt it, using the provided keyring. Checks that the frames in the 97 | // armor are as expected. Returns the sender key recovered during message 98 | // processing, the plaintext (if decryption succeeded), the armor branding, and 99 | // maybe an error if there was a failure. 100 | func Dearmor62SigncryptOpen(ciphertext string, keyring SigncryptKeyring, resolver SymmetricKeyResolver) (SigningPublicKey, []byte, string, error) { 101 | buf := bytes.NewBufferString(ciphertext) 102 | mki, s, brand, err := NewDearmor62SigncryptOpenStream(buf, keyring, resolver) 103 | if err != nil { 104 | return mki, nil, "", err 105 | } 106 | out, err := io.ReadAll(s) 107 | if err != nil { 108 | return mki, nil, "", err 109 | } 110 | return mki, out, brand, nil 111 | } 112 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "crypto/hmac" 8 | ) 9 | 10 | // RawBoxKey is the raw byte-representation of what a box key should 11 | // look like, a static 32-byte buffer. Used for NaCl Box. 12 | type RawBoxKey [32]byte 13 | 14 | func rawBoxKeyFromSlice(slice []byte) (*RawBoxKey, error) { 15 | var result RawBoxKey 16 | if len(slice) != len(result) { 17 | return nil, ErrBadBoxKey 18 | } 19 | result = sliceToByte32(slice) 20 | return &result, nil 21 | } 22 | 23 | // SymmetricKey is a template for a symmetric key, a 32-byte static 24 | // buffer. Used for NaCl SecretBox. 25 | type SymmetricKey [32]byte 26 | 27 | func newRandomSymmetricKey() (*SymmetricKey, error) { 28 | var s SymmetricKey 29 | err := csprngRead(s[:]) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return &s, nil 34 | } 35 | 36 | func symmetricKeyFromSlice(slice []byte) (*SymmetricKey, error) { 37 | var result SymmetricKey 38 | if len(slice) != len(result) { 39 | return nil, ErrBadSymmetricKey 40 | } 41 | result = sliceToByte32(slice) 42 | return &result, nil 43 | } 44 | 45 | // EphemeralKeyCreator is an interface for objects that can create 46 | // random ephemeral keys. 47 | type EphemeralKeyCreator interface { 48 | // CreateEmphemeralKey creates a random ephemeral key. 49 | CreateEphemeralKey() (BoxSecretKey, error) 50 | } 51 | 52 | // BasePublicKey types can output a key ID corresponding to the key. 53 | type BasePublicKey interface { 54 | // ToKID outputs the "key ID" that corresponds to this key. 55 | // You can do whatever you'd like here, but probably it makes sense just 56 | // to output the public key as is. 57 | ToKID() []byte 58 | } 59 | 60 | // BoxPublicKey is an generic interface to NaCl's public key Box function. 61 | type BoxPublicKey interface { 62 | BasePublicKey 63 | // Implement EphemeralKeyCreator to avoid breaking the public API. 64 | EphemeralKeyCreator 65 | 66 | // ToRawBoxKeyPointer returns this public key as a *[32]byte, 67 | // for use with nacl.box.Seal 68 | ToRawBoxKeyPointer() *RawBoxKey 69 | 70 | // HideIdentity returns true if we should hide the identity of this 71 | // key in our output message format. 72 | HideIdentity() bool 73 | } 74 | 75 | // BoxPrecomputedSharedKey results from a Precomputation below. 76 | type BoxPrecomputedSharedKey interface { 77 | Unbox(nonce Nonce, msg []byte) ([]byte, error) 78 | Box(nonce Nonce, msg []byte) []byte 79 | } 80 | 81 | // BoxSecretKey is the secret key corresponding to a BoxPublicKey 82 | type BoxSecretKey interface { 83 | // Box boxes up data, sent from this secret key, and to the receiver 84 | // specified. 85 | Box(receiver BoxPublicKey, nonce Nonce, msg []byte) []byte 86 | 87 | // Unbox opens up the box, using this secret key as the receiver key 88 | // abd the give public key as the sender key. 89 | Unbox(sender BoxPublicKey, nonce Nonce, msg []byte) ([]byte, error) 90 | 91 | // GetPublicKey gets the public key associated with this secret key. 92 | GetPublicKey() BoxPublicKey 93 | 94 | // Precompute computes a DH with the given key 95 | Precompute(peer BoxPublicKey) BoxPrecomputedSharedKey 96 | } 97 | 98 | // SigningSecretKey is a secret NaCl key that can sign messages. 99 | type SigningSecretKey interface { 100 | // Sign signs message with this secret key. 101 | Sign(message []byte) ([]byte, error) 102 | 103 | // GetPublicKey gets the public key associated with this secret key. 104 | GetPublicKey() SigningPublicKey 105 | } 106 | 107 | // SigningPublicKey is a public NaCl key that can verify 108 | // signatures. 109 | type SigningPublicKey interface { 110 | BasePublicKey 111 | 112 | // Verify verifies that signature is a valid signature of message for 113 | // this public key. 114 | Verify(message []byte, signature []byte) error 115 | } 116 | 117 | // Keyring is an interface used with decryption; it is called to 118 | // recover public or private keys during the decryption process. 119 | // Calls can block on network action. 120 | type Keyring interface { 121 | // Implement EphemeralKeyCreator to avoid breaking the public API. 122 | EphemeralKeyCreator 123 | 124 | // LookupBoxSecretKey looks in the Keyring for the secret key corresponding 125 | // to one of the given Key IDs. Returns the index and the key on success, 126 | // or -1 and nil on failure. 127 | LookupBoxSecretKey(kids [][]byte) (int, BoxSecretKey) 128 | 129 | // LookupBoxPublicKey returns a public key given the specified key ID. 130 | // For most cases, the key ID will be the key itself. 131 | LookupBoxPublicKey(kid []byte) BoxPublicKey 132 | 133 | // GetAllSecretKeys returns all keys, needed if we want to support 134 | // "hidden" receivers via trial and error 135 | GetAllBoxSecretKeys() []BoxSecretKey 136 | 137 | // ImportEphemeralKey imports the ephemeral key into 138 | // BoxPublicKey format. This key has never been seen before, so 139 | // will be ephemeral. 140 | ImportBoxEphemeralKey(kid []byte) BoxPublicKey 141 | } 142 | 143 | // SigKeyring is an interface used during verification to find 144 | // the public key for the signer of a message. 145 | type SigKeyring interface { 146 | // LookupSigningPublicKey returns a public signing key for the specified key ID. 147 | LookupSigningPublicKey(kid []byte) SigningPublicKey 148 | } 149 | 150 | // PublicKeyEqual returns true if the two public keys are equal. 151 | func PublicKeyEqual(k1, k2 BasePublicKey) bool { 152 | return hmac.Equal(k1.ToKID(), k2.ToKID()) 153 | } 154 | -------------------------------------------------------------------------------- /tweakable_signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | type testSignOptions struct { 13 | corruptHeader func(sh *SignatureHeader) 14 | corruptHeaderBytes func(bytes *[]byte) 15 | swapBlock bool 16 | skipBlock func(blockNum packetSeqno) bool 17 | skipFooter bool 18 | } 19 | 20 | type testSignStream struct { 21 | version Version 22 | headerHash headerHash 23 | encoder encoder 24 | buffer bytes.Buffer 25 | seqno packetSeqno 26 | secretKey SigningSecretKey 27 | options testSignOptions 28 | savedBlock interface{} 29 | } 30 | 31 | func newTestSignStream(version Version, w io.Writer, signer SigningSecretKey, opts testSignOptions) (*testSignStream, error) { 32 | if signer == nil { 33 | return nil, ErrInvalidParameter{message: "no signing key provided"} 34 | } 35 | 36 | header, err := newSignatureHeader(version, signer.GetPublicKey(), MessageTypeAttachedSignature) 37 | if err != nil { 38 | return nil, err 39 | } 40 | if opts.corruptHeader != nil { 41 | opts.corruptHeader(header) 42 | } 43 | 44 | // Encode the header bytes. 45 | headerBytes, err := encodeToBytes(header) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if opts.corruptHeaderBytes != nil { 50 | opts.corruptHeaderBytes(&headerBytes) 51 | } 52 | 53 | // Compute the header hash. 54 | headerHash := hashHeader(headerBytes) 55 | 56 | stream := &testSignStream{ 57 | version: version, 58 | headerHash: headerHash, 59 | encoder: newEncoder(w), 60 | secretKey: signer, 61 | options: opts, 62 | } 63 | 64 | // Double encode the header bytes onto the wire. 65 | err = stream.encoder.Encode(headerBytes) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return stream, nil 71 | } 72 | 73 | func (s *testSignStream) Write(p []byte) (int, error) { 74 | n, err := s.buffer.Write(p) 75 | if err != nil { 76 | return 0, err 77 | } 78 | 79 | // If s.buffer.Len() == signatureBlockSize, we don't want to 80 | // write it out just yet, since for V2 we need to be sure this 81 | // isn't the last block. 82 | for s.buffer.Len() > signatureBlockSize { 83 | if err := s.signBlock(false); err != nil { 84 | return 0, err 85 | } 86 | } 87 | 88 | return n, nil 89 | } 90 | 91 | func (s *testSignStream) Close() error { 92 | switch s.version { 93 | case Version1(): 94 | if s.buffer.Len() > 0 { 95 | if err := s.signBlock(false); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | if s.buffer.Len() > 0 { 101 | panic(fmt.Sprintf("s.buffer.Len()=%d > 0", s.buffer.Len())) 102 | } 103 | 104 | if s.options.skipFooter { 105 | return nil 106 | } 107 | 108 | return s.signBlock(true) 109 | 110 | case Version2(): 111 | isFinal := true 112 | 113 | if s.options.skipFooter { 114 | isFinal = false 115 | } 116 | 117 | if err := s.signBlock(isFinal); err != nil { 118 | return err 119 | } 120 | 121 | if s.buffer.Len() > 0 { 122 | panic(fmt.Sprintf("s.buffer.Len()=%d > 0", s.buffer.Len())) 123 | } 124 | 125 | return nil 126 | 127 | default: 128 | panic(ErrBadVersion{s.version}) 129 | } 130 | } 131 | 132 | func (s *testSignStream) signBlock(isFinal bool) error { 133 | chunk := s.buffer.Next(signatureBlockSize) 134 | checkSignBlockRead(s.version, isFinal, signatureBlockSize, len(chunk), s.buffer.Len()) 135 | 136 | sig, err := s.computeSig(chunk, s.seqno, isFinal) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | assertEncodedChunkState(s.version, chunk, 0, uint64(s.seqno), isFinal) 142 | 143 | sBlock := makeSignatureBlock(s.version, sig, chunk, isFinal) 144 | 145 | if s.options.swapBlock { 146 | if s.seqno == 0 { 147 | s.savedBlock = sBlock 148 | s.seqno++ 149 | return nil 150 | } 151 | } 152 | 153 | if s.options.skipBlock == nil || !s.options.skipBlock(s.seqno) { 154 | if err := s.encoder.Encode(sBlock); err != nil { 155 | return err 156 | } 157 | s.seqno++ 158 | } 159 | 160 | if s.options.swapBlock { 161 | if s.savedBlock != nil { 162 | if err := s.encoder.Encode(s.savedBlock); err != nil { 163 | return err 164 | } 165 | s.savedBlock = nil 166 | return nil 167 | } 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (s *testSignStream) computeSig(payloadChunk []byte, seqno packetSeqno, isFinal bool) ([]byte, error) { 174 | return s.secretKey.Sign(attachedSignatureInput(s.version, s.headerHash, payloadChunk, seqno, isFinal)) 175 | } 176 | 177 | func testTweakSign(version Version, plaintext []byte, signer SigningSecretKey, opts testSignOptions) ([]byte, error) { 178 | var buf bytes.Buffer 179 | s, err := newTestSignStream(version, &buf, signer, opts) 180 | if err != nil { 181 | return nil, err 182 | } 183 | if _, err := s.Write(plaintext); err != nil { 184 | return nil, err 185 | } 186 | if err := s.Close(); err != nil { 187 | return nil, err 188 | } 189 | return buf.Bytes(), nil 190 | } 191 | 192 | func testTweakSignDetached(version Version, plaintext []byte, signer SigningSecretKey, opts testSignOptions) ([]byte, error) { 193 | if signer == nil { 194 | return nil, ErrInvalidParameter{message: "no signing key provided"} 195 | } 196 | header, err := newSignatureHeader(version, signer.GetPublicKey(), MessageTypeDetachedSignature) 197 | if err != nil { 198 | return nil, err 199 | } 200 | 201 | if opts.corruptHeader != nil { 202 | opts.corruptHeader(header) 203 | } 204 | 205 | // Encode the header bytes. 206 | headerBytes, err := encodeToBytes(header) 207 | if err != nil { 208 | return nil, err 209 | } 210 | 211 | // Compute the header hash. 212 | headerHash := hashHeader(headerBytes) 213 | 214 | // Double encode the header bytes to start the output. 215 | output, err := encodeToBytes(headerBytes) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | // Sign the plaintext. 221 | signature, err := signer.Sign(detachedSignatureInput(headerHash, plaintext)) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | // Append the encoded signature to the output. 227 | encodedSig, err := encodeToBytes(signature) 228 | if err != nil { 229 | return nil, err 230 | } 231 | output = append(output, encodedSig...) 232 | 233 | return output, nil 234 | } 235 | -------------------------------------------------------------------------------- /verify_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "encoding/hex" 9 | "errors" 10 | "io" 11 | "sync" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestVerifyVersionValidator(t *testing.T) { 19 | in := []byte{0x01} 20 | key := newSigPrivKey(t) 21 | smg, err := Sign(Version1(), in, key) 22 | require.NoError(t, err) 23 | 24 | _, _, err = Verify(SingleVersionValidator(Version2()), smg, kr) 25 | require.NotNil(t, err) 26 | } 27 | 28 | func testVerify(t *testing.T, version Version) { 29 | in := randomMsg(t, 128) 30 | key := newSigPrivKey(t) 31 | smsg, err := Sign(version, in, key) 32 | require.NoError(t, err) 33 | skey, msg, err := Verify(SingleVersionValidator(version), smsg, kr) 34 | require.NoError(t, err, "input: %x\nsigned msg: %x", in, smsg) 35 | assert.True(t, PublicKeyEqual(skey, key.GetPublicKey()), 36 | "sender key %x, expected %x", skey.ToKID(), key.GetPublicKey().ToKID()) 37 | assert.Equal(t, in, msg) 38 | } 39 | 40 | func testVerifyNewMinorVersion(t *testing.T, version Version) { 41 | in := []byte{0x01} 42 | 43 | newVersion := version 44 | newVersion.Minor++ 45 | 46 | tso := testSignOptions{ 47 | corruptHeader: func(sh *SignatureHeader) { 48 | sh.Version = newVersion 49 | }, 50 | } 51 | key := newSigPrivKey(t) 52 | smg, err := testTweakSign(version, in, key, tso) 53 | require.NoError(t, err) 54 | 55 | _, _, err = Verify(SingleVersionValidator(newVersion), smg, kr) 56 | require.NoError(t, err) 57 | } 58 | 59 | func testVerifyConcurrent(t *testing.T, version Version) { 60 | in := randomMsg(t, 128) 61 | key := newSigPrivKey(t) 62 | smsg, err := Sign(version, in, key) 63 | require.NoError(t, err) 64 | 65 | var wg sync.WaitGroup 66 | for i := 0; i < 100; i++ { 67 | wg.Add(1) 68 | go func() { 69 | defer wg.Done() 70 | skey, msg, err := Verify(SingleVersionValidator(version), smsg, kr) 71 | if !assert.NoError(t, err, "input: %x\nsigned msg: %x", in, smsg) { 72 | // Don't fall through, as the tests below will panic. 73 | return 74 | } 75 | assert.True(t, PublicKeyEqual(skey, key.GetPublicKey()), 76 | "sender key %x, expected %x", skey.ToKID(), key.GetPublicKey().ToKID()) 77 | assert.Equal(t, in, msg) 78 | }() 79 | } 80 | wg.Wait() 81 | } 82 | 83 | type emptySigKeyring struct{} 84 | 85 | func (k emptySigKeyring) LookupSigningPublicKey(_ []byte) SigningPublicKey { return nil } 86 | 87 | func testVerifyEmptyKeyring(t *testing.T, version Version) { 88 | in := randomMsg(t, 128) 89 | key := newSigPrivKey(t) 90 | smsg, err := Sign(version, in, key) 91 | require.NoError(t, err) 92 | 93 | _, _, err = Verify(SingleVersionValidator(version), smsg, emptySigKeyring{}) 94 | require.Equal(t, ErrNoSenderKey{Sender: key.GetPublicKey().ToKID()}, err) 95 | } 96 | 97 | func testVerifyDetachedEmptyKeyring(t *testing.T, version Version) { 98 | key := newSigPrivKey(t) 99 | msg := randomMsg(t, 128) 100 | sig, err := SignDetached(version, msg, key) 101 | require.NoError(t, err) 102 | 103 | _, err = VerifyDetached(SingleVersionValidator(version), msg, sig, emptySigKeyring{}) 104 | require.Equal(t, ErrNoSenderKey{Sender: key.GetPublicKey().ToKID()}, err) 105 | } 106 | 107 | func testVerifyErrorAtEOF(t *testing.T, version Version) { 108 | in := randomMsg(t, 128) 109 | key := newSigPrivKey(t) 110 | smsg, err := Sign(version, in, key) 111 | require.NoError(t, err) 112 | 113 | var reader io.Reader = bytes.NewReader(smsg) 114 | errAtEOF := errors.New("err at EOF") 115 | reader = errAtEOFReader{reader, errAtEOF} 116 | _, stream, err := NewVerifyStream(SingleVersionValidator(version), reader, kr) 117 | require.NoError(t, err) 118 | 119 | msg, err := io.ReadAll(stream) 120 | requireErrSuffix(t, err, errAtEOF.Error()) 121 | 122 | // Since the bytes are still verified, the verified message 123 | // should still compare equal to the original input. 124 | assert.Equal(t, in, msg) 125 | } 126 | 127 | func TestVerify(t *testing.T) { 128 | tests := []func(*testing.T, Version){ 129 | testVerify, 130 | testVerifyNewMinorVersion, 131 | testVerifyConcurrent, 132 | testVerifyEmptyKeyring, 133 | testVerifyDetachedEmptyKeyring, 134 | testVerifyErrorAtEOF, 135 | } 136 | runTestsOverVersions(t, "test", tests) 137 | } 138 | 139 | type pubkeyOnlySigKeyring struct{} 140 | 141 | func (p pubkeyOnlySigKeyring) LookupSigningPublicKey(kid []byte) SigningPublicKey { 142 | return newSigPubKey(kid) 143 | } 144 | 145 | const hardcodedV1SignedMessage = ` 146 | BEGIN KEYBASE SALTPACK SIGNED MESSAGE. kXR7VktZdyH7rvq v5wcIkHbsMGwMrf 147 | bu4PmUTnBUI2QWi Nu9smFqPCiRfB9h PAUmWFHLkTKGMdN tdrKMtkDu0UhJEj 7gM6Tt8OeykFHq9 148 | R4FnzgakB19YwYa CGVfWxxXpK9OaMI S00BurzWOWBXIxe EoTHvgyx1oHUVdX HRNjJCXTvsSJVa8 149 | Qyg3bN37HAfS8ek gZG6JflV06S2Olp gLdhxNZKIo2zF9P sD5pDFXvoVVzeNC D4vZtMiNQrniEYo 150 | qY903nTYqyGQ4yl UULZ6yP14CcSPfg 8r8CXVi5Z2. END KEYBASE SALTPACK SIGNED 151 | MESSAGE. 152 | ` 153 | 154 | const hardcodedVerifyKey = "f596585d050597c03a87d653c4be89f7327dbd86b921dd05acfc9df33eb7a962" 155 | 156 | func TestHardcodedSignedMessageV1(t *testing.T) { 157 | decodedKey, err := hex.DecodeString(hardcodedVerifyKey) 158 | require.NoError(t, err) 159 | keyring := pubkeyOnlySigKeyring{} 160 | signer, plaintext, _, err := Dearmor62Verify(SingleVersionValidator(Version1()), hardcodedV1SignedMessage, keyring) 161 | require.NoError(t, err) 162 | require.Equal(t, "test message!", string(plaintext)) 163 | require.Equal(t, decodedKey, signer.ToKID()) 164 | } 165 | 166 | const hardcodedV1DetachedSignature = ` 167 | BEGIN KEYBASE SALTPACK DETACHED SIGNATURE. kXR7VktZdyH7rvq v5wcIkPOwOUCix9 168 | HfoZZdGgIjzeYWi Nu9smFqPCiRfB9h PAUmWFHLkUaLXQd DTZrK37uaKi9dgf 60zJCgqbheQLTVP 169 | Vr2Dw2x1MLOenwI dt3P0dRsyh2WvQW OeqH28kbuzPiA0f OPQ0Y26dpV8A8uE DUDdJed0edSYEbx 170 | v. END KEYBASE SALTPACK DETACHED SIGNATURE. 171 | ` 172 | 173 | func TestHardcodedDetachedSignatureV1(t *testing.T) { 174 | decodedKey, err := hex.DecodeString(hardcodedVerifyKey) 175 | require.NoError(t, err) 176 | keyring := pubkeyOnlySigKeyring{} 177 | signer, _, err := Dearmor62VerifyDetached(SingleVersionValidator(Version1()), []byte("test message!"), hardcodedV1DetachedSignature, keyring) 178 | require.NoError(t, err) 179 | require.Equal(t, decodedKey, signer.ToKID()) 180 | } 181 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | var ( 12 | // ErrNoDecryptionKey is an error indicating no decryption key was found for the 13 | // incoming message. You'll get one of these if you respond to a Keyring.LookupSecretBoxKey 14 | // request with a (-1,nil) return value, and no hidden keys are found. 15 | ErrNoDecryptionKey = errors.New("no decryption key found for message") 16 | 17 | // ErrTrailingGarbage indicates that additional msgpack packets were found after the 18 | // end of the encryption stream. 19 | ErrTrailingGarbage = errors.New("trailing garbage found at end of message") 20 | 21 | // ErrFailedToReadHeaderBytes indicates that we failed to read the 22 | // doubly-encoded header bytes object from the input stream. 23 | ErrFailedToReadHeaderBytes = errors.New("failed to read header bytes") 24 | 25 | // ErrPacketOverflow indicates that more than (2^64-2) packets were found in an encryption 26 | // stream. This would indicate a very big message, and results in an error here. 27 | ErrPacketOverflow = errors.New("no more than 2^32 packets in a message are supported") 28 | 29 | // ErrInsufficientRandomness is generated when the encryption fails to collect 30 | // enough randomness to proceed. We're using the standard crypto/rand source 31 | // of randomness, so this should never happen 32 | ErrInsufficientRandomness = errors.New("could not collect enough randomness") 33 | 34 | // ErrBadEphemeralKey is for when an ephemeral key fails to be properly 35 | // imported. 36 | ErrBadEphemeralKey = errors.New("bad ephermal key in header") 37 | 38 | // ErrBadReceivers shows up when you pass a bad receivers vector 39 | ErrBadReceivers = errors.New("bad receivers argument") 40 | 41 | // ErrBadSenderKeySecretbox is returned if the sender secretbox fails to 42 | // open. 43 | ErrBadSenderKeySecretbox = errors.New("sender secretbox failed to open") 44 | 45 | // ErrBadSymmetricKey is returned if a key with the wrong number of bytes 46 | // is discovered in the encryption header. 47 | ErrBadSymmetricKey = errors.New("bad symmetric key; must be 32 bytes") 48 | 49 | // ErrBadBoxKey is returned if a key with the wrong number of bytes 50 | // is discovered in the encryption header. 51 | ErrBadBoxKey = errors.New("bad box key; must be 32 bytes") 52 | 53 | // ErrBadLookup is when the user-provided key lookup gives a bad value 54 | ErrBadLookup = errors.New("bad key lookup") 55 | 56 | // ErrBadSignature is returned when verification of a block fails. 57 | ErrBadSignature = errors.New("invalid signature") 58 | 59 | // ErrDecryptionFailed is returned when a decryption fails 60 | ErrDecryptionFailed = errors.New("decryption failed") 61 | 62 | // ErrWrongNumberOfKeys is returned when the resolved list of keys isn't 63 | // the same length as the identifiers list. 64 | ErrWrongNumberOfKeys = errors.New("wrong number of resolved keys") 65 | 66 | // ErrUnexpectedEmptyBlock is returned when an empty block is 67 | // encountered that isn't both the last one and the first one 68 | // (for V2 and higher), or isn't the last one (for V1). 69 | ErrUnexpectedEmptyBlock = errors.New("unexpected empty block") 70 | 71 | // ErrShortSliceOrBuffer is returned when the input slice or buffer provided 72 | // is too short to determine if it is the beginning of a binary saltpack message 73 | ErrShortSliceOrBuffer = errors.New("the slice or buffer is too short to tell if it is the beginning of a saltpack message") 74 | 75 | // ErrNotASaltpackMessage is returned when the message given as input is not 76 | // a valid saltpack message 77 | ErrNotASaltpackMessage = errors.New("not a saltpack message") 78 | ) 79 | 80 | // ErrNoSenderKey indicates that on decryption/verification we couldn't find a public key 81 | // for the sender. 82 | type ErrNoSenderKey struct { 83 | Sender []byte 84 | } 85 | 86 | // ErrBadTag is generated when a payload hash doesn't match the hash 87 | // authenticator. It specifies which Packet sequence number the bad packet was 88 | // in. 89 | type ErrBadTag packetSeqno 90 | 91 | // ErrBadCiphertext is generated when decryption fails due to improper authentication. It specifies 92 | // which Packet sequence number the bad packet was in. 93 | type ErrBadCiphertext packetSeqno 94 | 95 | // ErrRepeatedKey is produced during encryption if a key is repeated; keys must be 96 | // unique. 97 | type ErrRepeatedKey []byte 98 | 99 | // ErrWrongMessageType is produced if one packet tag was expected, but a packet 100 | // of another tag was found. 101 | type ErrWrongMessageType struct { 102 | Wanted MessageType 103 | Received MessageType 104 | } 105 | 106 | // ErrBadVersion is returned if a packet of an unsupported version is found. 107 | // Current, only Version1 is supported. 108 | type ErrBadVersion struct { 109 | received Version 110 | } 111 | 112 | // ErrBadFrame shows up when the BEGIN or END frames have issues 113 | type ErrBadFrame struct { 114 | msg string 115 | } 116 | 117 | func (e ErrBadFrame) Error() string { 118 | return fmt.Sprintf("Error in framing: %s", e.msg) 119 | } 120 | 121 | func makeErrBadFrame(format string, args ...interface{}) error { 122 | return ErrBadFrame{fmt.Sprintf(format, args...)} 123 | } 124 | 125 | func (e ErrNoSenderKey) Error() string { 126 | return "no sender key found for message" 127 | } 128 | 129 | func (e ErrWrongMessageType) Error() string { 130 | return fmt.Sprintf("Wrong saltpack message type: wanted %s, but got %s instead", e.Wanted, e.Received) 131 | } 132 | 133 | func (e ErrBadVersion) Error() string { 134 | return fmt.Sprintf("Unsupported version (%s)", e.received) 135 | } 136 | 137 | func (e ErrBadCiphertext) Error() string { 138 | return fmt.Sprintf("In packet %d: bad ciphertext; failed Poly1305", e) 139 | } 140 | 141 | func (e ErrBadTag) Error() string { 142 | return fmt.Sprintf("In packet %d: bad Poly1305 tag; data was corrupted in transit", e) 143 | } 144 | 145 | func (e ErrRepeatedKey) Error() string { 146 | return fmt.Sprintf("Repeated recipient key: %x", []byte(e)) 147 | } 148 | 149 | // ErrInvalidParameter signifies that a function was called with 150 | // an invalid parameter. 151 | type ErrInvalidParameter struct { 152 | message string 153 | } 154 | 155 | func (e ErrInvalidParameter) Error() string { 156 | return fmt.Sprintf("Invalid parameter: %s", e.message) 157 | } 158 | -------------------------------------------------------------------------------- /packets_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/keybase/go-codec/codec" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | // Test that ints in headers are encoded as positive fixnums. 14 | func TestHeaderHardcoded(t *testing.T) { 15 | header := EncryptionHeader{ 16 | Version: Version2(), 17 | Type: MessageTypeDetachedSignature, 18 | } 19 | 20 | expectedBytes := []byte{0x96, 0xa0, 0x92, 0x2, 0x0, 0x2, 0xc0, 0xc0, 0xc0} 21 | bytes, err := encodeToBytes(header) 22 | require.NoError(t, err) 23 | require.Equal(t, expectedBytes, bytes) 24 | } 25 | 26 | // Test that strings are encoded with the fewest number of bytes. 27 | func TestEncodedStringLength(t *testing.T) { 28 | s := string(make([]byte, 31)) 29 | bytes, err := encodeToBytes(s) 30 | require.NoError(t, err) 31 | require.Equal(t, 31+1, len(bytes)) 32 | 33 | s = string(make([]byte, 255)) 34 | bytes, err = encodeToBytes(s) 35 | require.NoError(t, err) 36 | require.Equal(t, 255+2, len(bytes)) 37 | 38 | s = string(make([]byte, 65535)) 39 | bytes, err = encodeToBytes(s) 40 | require.NoError(t, err) 41 | require.Equal(t, 65535+3, len(bytes)) 42 | } 43 | 44 | // Test that byte arrays are encoded with the fewest number of bytes. 45 | func TestEncodedByteArrayLength(t *testing.T) { 46 | b := make([]byte, 255) 47 | bytes, err := encodeToBytes(b) 48 | require.NoError(t, err) 49 | require.Equal(t, 255+2, len(bytes)) 50 | 51 | b = make([]byte, 65535) 52 | bytes, err = encodeToBytes(b) 53 | require.NoError(t, err) 54 | require.Equal(t, 65535+3, len(bytes)) 55 | } 56 | 57 | // Test that generic arrays are encoded with the fewest number of bytes. 58 | func TestEncodedArrayLength(t *testing.T) { 59 | // A [1]bool should encode as two bytes. 60 | b := make([][1]bool, 15) 61 | bytes, err := encodeToBytes(b) 62 | require.NoError(t, err) 63 | require.Equal(t, 2*15+1, len(bytes)) 64 | 65 | b = make([][1]bool, 65535) 66 | bytes, err = encodeToBytes(b) 67 | require.NoError(t, err) 68 | require.Equal(t, 65535*2+3, len(bytes)) 69 | } 70 | 71 | // Test that encryptionBlockV2 encodes and decodes properly. 72 | func TestEncryptionBlockV2RoundTrip(t *testing.T) { 73 | isFinal := false 74 | hashAuthenticators := []payloadAuthenticator{{0x1}, {0x2}} 75 | payloadCiphertext := []byte("TestEncryptionBlockV2RoundTrip") 76 | 77 | blockV2 := encryptionBlockV2{ 78 | encryptionBlockV1: encryptionBlockV1{ 79 | HashAuthenticators: hashAuthenticators, 80 | PayloadCiphertext: payloadCiphertext, 81 | }, 82 | IsFinal: isFinal, 83 | } 84 | 85 | h := codecHandle() 86 | 87 | var blockV2Bytes1 []byte 88 | encoder := codec.NewEncoderBytes(&blockV2Bytes1, h) 89 | blockV2.CodecEncodeSelf(encoder) 90 | 91 | blockV2Bytes2, err := encodeToBytes(blockV2) 92 | require.NoError(t, err) 93 | 94 | require.Equal(t, blockV2Bytes1, blockV2Bytes2) 95 | 96 | var blockV2Decoded1 encryptionBlockV2 97 | decoder := codec.NewDecoderBytes(blockV2Bytes1, h) 98 | blockV2Decoded1.CodecDecodeSelf(decoder) 99 | require.Equal(t, blockV2, blockV2Decoded1) 100 | 101 | var blockV2Decoded2 encryptionBlockV2 102 | err = decodeFromBytes(&blockV2Decoded2, blockV2Bytes1) 103 | require.NoError(t, err) 104 | 105 | require.Equal(t, blockV2, blockV2Decoded2) 106 | } 107 | 108 | // Test that the encoded field order for encryptionBlockV2 puts 109 | // IsFinal first. 110 | func TestEncryptionBlockV2FieldOrder(t *testing.T) { 111 | isFinal := true 112 | hashAuthenticators := []payloadAuthenticator{{0x3}, {0x4}} 113 | payloadCiphertext := []byte("TestEncryptionBlockV2FieldOrder") 114 | 115 | blockV2 := encryptionBlockV2{ 116 | encryptionBlockV1: encryptionBlockV1{ 117 | HashAuthenticators: hashAuthenticators, 118 | PayloadCiphertext: payloadCiphertext, 119 | }, 120 | IsFinal: isFinal, 121 | } 122 | 123 | blockV2Bytes, err := encodeToBytes(blockV2) 124 | require.NoError(t, err) 125 | 126 | var blockV2Decoded encryptionBlockV2 127 | err = decodeFromBytes([]interface{}{ 128 | &blockV2Decoded.IsFinal, 129 | &blockV2Decoded.HashAuthenticators, 130 | &blockV2Decoded.PayloadCiphertext, 131 | }, blockV2Bytes) 132 | require.NoError(t, err) 133 | 134 | require.Equal(t, blockV2, blockV2Decoded) 135 | } 136 | 137 | // Test that signatureBlockV2 encodes and decodes properly. 138 | func TestSignatureBlockV2RoundTrip(t *testing.T) { 139 | isFinal := false 140 | signature := []byte("TestSignatureBlockV2RoundTrip signature") 141 | payloadChunk := []byte("TestSignatureBlockV2RoundTrip payload") 142 | 143 | blockV2 := signatureBlockV2{ 144 | signatureBlockV1: signatureBlockV1{ 145 | Signature: signature, 146 | PayloadChunk: payloadChunk, 147 | }, 148 | IsFinal: isFinal, 149 | } 150 | 151 | h := codecHandle() 152 | 153 | var blockV2Bytes1 []byte 154 | encoder := codec.NewEncoderBytes(&blockV2Bytes1, h) 155 | blockV2.CodecEncodeSelf(encoder) 156 | 157 | blockV2Bytes2, err := encodeToBytes(blockV2) 158 | require.NoError(t, err) 159 | 160 | require.Equal(t, blockV2Bytes1, blockV2Bytes2) 161 | 162 | var blockV2Decoded1 signatureBlockV2 163 | decoder := codec.NewDecoderBytes(blockV2Bytes1, h) 164 | blockV2Decoded1.CodecDecodeSelf(decoder) 165 | require.Equal(t, blockV2, blockV2Decoded1) 166 | 167 | var blockV2Decoded2 signatureBlockV2 168 | err = decodeFromBytes(&blockV2Decoded2, blockV2Bytes1) 169 | require.NoError(t, err) 170 | 171 | require.Equal(t, blockV2, blockV2Decoded2) 172 | } 173 | 174 | // Test that the encoded field order for signatureBlockV2 puts 175 | // IsFinal first. 176 | func TestSignatureBlockV2FieldOrder(t *testing.T) { 177 | isFinal := true 178 | signature := []byte("TestSignatureBlockV2FieldOrder signature") 179 | payloadChunk := []byte("TestSignatureBlockV2FieldOrder payload") 180 | 181 | blockV2 := signatureBlockV2{ 182 | signatureBlockV1: signatureBlockV1{ 183 | Signature: signature, 184 | PayloadChunk: payloadChunk, 185 | }, 186 | IsFinal: isFinal, 187 | } 188 | 189 | blockV2Bytes, err := encodeToBytes(blockV2) 190 | require.NoError(t, err) 191 | 192 | var blockV2Decoded signatureBlockV2 193 | err = decodeFromBytes([]interface{}{ 194 | &blockV2Decoded.IsFinal, 195 | &blockV2Decoded.Signature, 196 | &blockV2Decoded.PayloadChunk, 197 | }, blockV2Bytes) 198 | require.NoError(t, err) 199 | 200 | require.Equal(t, blockV2, blockV2Decoded) 201 | } 202 | -------------------------------------------------------------------------------- /specs/saltpack_signing_v2.md: -------------------------------------------------------------------------------- 1 | # Saltpack Binary Signing Format [version 2] 2 | 3 | As with the encryption format, we want our signing format to have some 4 | properties on top of a standard NaCl signature: 5 | - Streaming. We want to be able to verify a message of any size, without 6 | fitting the whole thing in RAM, and without requiring a second pass to output 7 | attached plaintext. But we should only ever output verified data. 8 | - Abuse resistance. Alice might use the same signing key for many applications 9 | besides saltpack. Mallory (an attacker) could [try to trick Alice into 10 | signing 11 | messages](https://blog.sandstorm.io/news/2015-05-01-is-that-ascii-or-protobuf.html) 12 | that are meaningful to other applications. Alice should avoid signing bytes 13 | that Mallory could have chosen. 14 | 15 | ## Design 16 | 17 | We define two signing formats: one attached and one detached. The attached 18 | format will have a header packet and payload packets similar to the encryption 19 | format, with a final packet flag at the end of the message, and an incrementing 20 | counter to prevent reordering. The detached format will contain just a header 21 | packet, with no payload. 22 | 23 | Both formats will hash a random nonce along with the plaintext, and then sign 24 | the resulting hash. This nonce serves two purposes. First, in the attached 25 | format, it prevents an attacker from swapping in payload packets from other 26 | messages. Second, in both formats, it helps us avoid signing bytes that an 27 | attacker could predict in advance. 28 | 29 | ## Attached Implementation 30 | 31 | When encoding strings, byte arrays, or arrays, pick the MessagePack 32 | encoding that will use the fewest number of bytes. 33 | 34 | An attached signature is a header packet, followed by any number of non-empty 35 | payload packets. An attached signing header packet is a [MessagePack 36 | array](https://github.com/msgpack/msgpack/blob/master/spec.md) that looks like 37 | this: 38 | 39 | ``` 40 | [ 41 | format name, 42 | version, 43 | mode, 44 | sender public key, 45 | nonce, 46 | ] 47 | ``` 48 | 49 | - **format name** is the string "saltpack". 50 | - **version** is a list of the major and minor versions, currently 51 | `[2, 0]`, both encoded as 52 | [positive fixnums](https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family). 53 | - **mode** is the number 1, for attached signing, encoded as a 54 | [positive fixnum](https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family). 55 | (0 is encryption, 2 is detached signing, and 3 is signcryption.) 56 | - **sender public key** is the sender's long-term NaCl signing public key, 32 bytes. 57 | - **nonce** is 32 random bytes. 58 | 59 | As in the encryption spec, the header packet is serialized into a MessagePack 60 | `array` object, hashed with SHA512 to produce the **header hash**, and then 61 | serialized *again* into a MessagePack `bin` object. 62 | 63 | Payload packets are MessagePack arrays that look like this: 64 | 65 | ``` 66 | [ 67 | final flag, 68 | signature, 69 | payload chunk, 70 | ] 71 | ``` 72 | 73 | - **final flag** is a boolean, true for the final payload packet, and false for 74 | all other payload packets. 75 | - **signature** is a detached NaCl signature, 64 bytes. 76 | - **payload chunk** is a chunk of the plaintext bytes, max size 1 MiB (= 2^20 77 | bytes). 78 | 79 | To make each signature, the sender first takes the SHA512 hash of the 80 | concatenation of four values: 81 | - the **header hash** from above 82 | - the packet sequence number, as a 64-bit big-endian unsigned integer, where 83 | the first payload packet is zero 84 | - the **final flag**, a 0x00 byte for false and a 0x01 byte for true 85 | - the **payload chunk** 86 | 87 | The sender then signs the concatenation of two values: 88 | - `"saltpack attached signature\0"` 89 | - the SHA512 hash above 90 | 91 | As in the encryption spec, the sender must verify that the last packet received 92 | sets the final packet flag. If the last packet of a message doesn't set the 93 | flag, the receiving client must report an error that the message has been 94 | truncated. 95 | 96 | ## Detached Implementation 97 | 98 | A detached signature header packet is equivalent to an attached signature 99 | header packet with a different mode (2 instead of 1). As above it's 100 | twice-encoded, with the **header hash** computed after the first encoding. A 101 | detached header is followed by a single MessagePack `bin` object, containing 102 | the 64-byte detached NaCl signature. 103 | 104 | ``` 105 | [ 106 | format name, 107 | version, 108 | mode, 109 | sender public key, 110 | nonce, 111 | ] 112 | 113 | signature 114 | ``` 115 | 116 | To make the signature, the sender first takes the SHA512 hash of the 117 | concatenation of two values:: 118 | - the **header hash** from above 119 | - the entire plaintext 120 | 121 | The sender then signs the concatenation of two values: 122 | - `"saltpack detached signature\0"` 123 | - the SHA512 hash above 124 | 125 | ## Examples 126 | 127 | An attached signature: 128 | 129 | ```yaml 130 | # header packet (on the wire, this is double-encoded) 131 | [ 132 | # format name 133 | "saltpack", 134 | # major and minor version 135 | [2, 0], 136 | # mode (1 = attached signing) 137 | 1, 138 | # sender public key 139 | 033e2d505c9ec79fb16fb40f34d743d5805dd36c844741420671fb57df24ed93, 140 | # nonce 141 | 22737333c279555e6e33a79b1ba06cab47d7689d65b43615393d6d607f1d7a52, 142 | ] 143 | 144 | # payload packet 145 | [ 146 | # signature 147 | bba4e5dff8cc6cea88e1d505d1f9a716ca0e79a142e26896493daf82733b4e220c555a0941e52673c25a384f334e0ccdcb62f89a4f01d13f0cb53961f0f4cc00, 148 | # payload chunk 149 | "Yea, though I walk through the valley of the shadow of death, I will fear no evil: for thou art with me; thy rod and thy staff they comfort me.", 150 | # final flag 151 | True, 152 | ] 153 | ``` 154 | 155 | A detached signature: 156 | 157 | ```yaml 158 | # header packet (on the wire, this is double-encoded) 159 | [ 160 | # format name 161 | "saltpack", 162 | # major and minor version 163 | [2, 0], 164 | # mode (2 = detached signing) 165 | 2, 166 | # sender public key 167 | 8a732edded5a23036c4c8caca6a04321f007b2e8b60cab975950a17b4c02ca76, 168 | # nonce 169 | 38d01af91e8cd9e6a79f19ea55733fea9778be327170d4fd8ed2074d4be0b784, 170 | ] 171 | 172 | # signature 173 | 6628f12374ce7c2a55d1c864e71206296e380586c9e61c3aa3ac1b5f11674bd53b895705183ff54d00fdeb5534b412569f58cb22dc6b3673b9a265e3bffe470d 174 | ``` 175 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/keybase/saltpack" 10 | "github.com/keybase/saltpack/basic" 11 | ) 12 | 13 | func ExampleEncryptArmor62Seal() { 14 | var err error 15 | 16 | // Make a new Keyring, initialized to be empty 17 | keyring := basic.NewKeyring() 18 | 19 | // The test message 20 | msg := []byte("The Magic Words are Squeamish Ossifrage") 21 | 22 | // Make a secret key for the sender 23 | var sender saltpack.BoxSecretKey 24 | sender, err = keyring.GenerateBoxKey() 25 | if err != nil { 26 | return 27 | } 28 | 29 | // And one for the receiver 30 | var receiver saltpack.BoxSecretKey 31 | receiver, err = keyring.GenerateBoxKey() 32 | if err != nil { 33 | return 34 | } 35 | 36 | // AllReceivers can contain more receivers (like the sender) 37 | // but for now, just the one. 38 | var ciphertext string 39 | allReceivers := []saltpack.BoxPublicKey{receiver.GetPublicKey()} 40 | ciphertext, err = saltpack.EncryptArmor62Seal(saltpack.CurrentVersion(), msg, sender, allReceivers, "") 41 | if err != nil { 42 | return 43 | } 44 | 45 | // The decrypted message should match the input mesasge. 46 | var msg2 []byte 47 | _, msg2, _, err = saltpack.Dearmor62DecryptOpen(saltpack.CheckKnownMajorVersion, ciphertext, keyring) 48 | if err != nil { 49 | return 50 | } 51 | 52 | fmt.Println(string(msg2)) 53 | 54 | // Output: 55 | // The Magic Words are Squeamish Ossifrage 56 | } 57 | 58 | func ExampleNewEncryptArmor62Stream() { 59 | var err error 60 | 61 | // Make a new Keyring, initialized to be empty 62 | keyring := basic.NewKeyring() 63 | 64 | // The test message 65 | plaintext := "The Magic Words are Squeamish Ossifrage" 66 | 67 | // Make a secret key for the sender 68 | var sender saltpack.BoxSecretKey 69 | sender, err = keyring.GenerateBoxKey() 70 | if err != nil { 71 | return 72 | } 73 | 74 | // And one for the receiver 75 | var receiver saltpack.BoxSecretKey 76 | receiver, err = keyring.GenerateBoxKey() 77 | if err != nil { 78 | return 79 | } 80 | 81 | // AllReceivers can contain more receivers (like the sender) 82 | // but for now, just the one. 83 | var output bytes.Buffer 84 | allReceivers := []saltpack.BoxPublicKey{receiver.GetPublicKey()} 85 | var input io.WriteCloser 86 | input, err = saltpack.NewEncryptArmor62Stream(saltpack.CurrentVersion(), &output, sender, allReceivers, "") 87 | if err != nil { 88 | return 89 | } 90 | // Write plaintext into the returned WriteCloser stream 91 | _, err = input.Write([]byte(plaintext)) 92 | if err != nil { 93 | return 94 | } 95 | // And close when we're done 96 | err = input.Close() 97 | if err != nil { 98 | return 99 | } 100 | 101 | // The decrypted message 102 | var plaintextOutput io.Reader 103 | _, plaintextOutput, _, err = saltpack.NewDearmor62DecryptStream(saltpack.CheckKnownMajorVersion, &output, keyring) 104 | if err != nil { 105 | return 106 | } 107 | 108 | // Copy all of the data out of the output decrypted stream, and into standard 109 | // output, here for testing / comparison purposes. 110 | _, err = io.Copy(os.Stdout, plaintextOutput) 111 | if err != nil { 112 | return 113 | } 114 | _, err = os.Stdout.Write([]byte{'\n'}) 115 | if err != nil { 116 | return 117 | } 118 | 119 | // Output: 120 | // The Magic Words are Squeamish Ossifrage 121 | } 122 | 123 | func ExampleSignArmor62() { 124 | var err error 125 | 126 | // Make a new Keyring, initialized to be empty 127 | keyring := basic.NewKeyring() 128 | 129 | // The test message 130 | msg := []byte("The Magic Words are Squeamish Ossifrage") 131 | 132 | // Make a secret key for the sender 133 | var signer saltpack.SigningSecretKey 134 | signer, err = keyring.GenerateSigningKey() 135 | if err != nil { 136 | return 137 | } 138 | 139 | var signed string 140 | signed, err = saltpack.SignArmor62(saltpack.CurrentVersion(), msg, signer, "") 141 | if err != nil { 142 | return 143 | } 144 | 145 | // The verified message should match the input mesasge. 146 | var verifiedMsg []byte 147 | var signingPublicKey saltpack.SigningPublicKey 148 | signingPublicKey, verifiedMsg, _, err = saltpack.Dearmor62Verify(saltpack.CheckKnownMajorVersion, signed, keyring) 149 | if err != nil { 150 | return 151 | } 152 | 153 | if saltpack.PublicKeyEqual(signingPublicKey, signer.GetPublicKey()) { 154 | fmt.Println("The right key") 155 | } 156 | 157 | fmt.Println(string(verifiedMsg)) 158 | 159 | // Output: 160 | // The right key 161 | // The Magic Words are Squeamish Ossifrage 162 | } 163 | 164 | func ExampleNewSignArmor62Stream() { 165 | var err error 166 | 167 | // Make a new Keyring, initialized to be empty 168 | keyring := basic.NewKeyring() 169 | 170 | // The test message 171 | msg := []byte("The Magic Words are Squeamish Ossifrage") 172 | 173 | // Make a secret key for the sender 174 | var signer saltpack.SigningSecretKey 175 | signer, err = keyring.GenerateSigningKey() 176 | if err != nil { 177 | return 178 | } 179 | 180 | // Make a new signature stream. We write the input data into 181 | // the input stream, and we read output out of the output stream. 182 | // In this case, the output stream is just a buffer. 183 | var input io.WriteCloser 184 | var output bytes.Buffer 185 | input, err = saltpack.NewSignArmor62Stream(saltpack.CurrentVersion(), &output, signer, "") 186 | if err != nil { 187 | return 188 | } 189 | 190 | // Write the message into the input stream, and then close 191 | _, err = input.Write(msg) 192 | if err != nil { 193 | return 194 | } 195 | err = input.Close() 196 | if err != nil { 197 | return 198 | } 199 | 200 | // The verified message. We pass the signed stream as the first argument 201 | // as a stream (here a bytes.Buffer which is output from above), and read the 202 | // verified data out of verified stream. 203 | var verifiedStream io.Reader 204 | var signingPublicKey saltpack.SigningPublicKey 205 | signingPublicKey, verifiedStream, _, err = saltpack.NewDearmor62VerifyStream(saltpack.CheckKnownMajorVersion, &output, keyring) 206 | if err != nil { 207 | return 208 | } 209 | 210 | // Assert we got the right key back. 211 | if saltpack.PublicKeyEqual(signingPublicKey, signer.GetPublicKey()) { 212 | fmt.Println("The right key") 213 | } 214 | 215 | // Copy all of the data out of the verified stream, and into standard 216 | // output, here for testing / comparison purposes. 217 | _, err = io.Copy(os.Stdout, verifiedStream) 218 | if err != nil { 219 | return 220 | } 221 | _, err = os.Stdout.Write([]byte{'\n'}) 222 | if err != nil { 223 | return 224 | } 225 | 226 | // Output: 227 | // The right key 228 | // The Magic Words are Squeamish Ossifrage 229 | } 230 | -------------------------------------------------------------------------------- /packets.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/keybase/go-codec/codec" 10 | ) 11 | 12 | type receiverKeys struct { 13 | _struct bool `codec:",toarray"` //nolint 14 | ReceiverKID []byte `codec:"receiver_key_id"` 15 | PayloadKeyBox []byte `codec:"payloadkey"` 16 | } 17 | 18 | // Version is a major.minor pair that shows the version of the whole file 19 | type Version struct { 20 | _struct bool `codec:",toarray"` //nolint 21 | Major int `codec:"major"` 22 | Minor int `codec:"minor"` 23 | } 24 | 25 | func (v Version) String() string { 26 | return fmt.Sprintf("%d.%d", v.Major, v.Minor) 27 | } 28 | 29 | // TODO: Check FormatName in the various Header.validate() functions. 30 | 31 | // EncryptionHeader is the first packet in an encrypted message. It contains 32 | // the encryptions of the session key, and various message metadata. This same 33 | // struct is used for the signcryption mode as well, though the key types 34 | // represented by the []byte arrays are different. (For example in the 35 | // signcryption mode, the sender secretbox contains a *signing* key instead of 36 | // an encryption key, and the receiver identifier takes a different form.) 37 | type EncryptionHeader struct { 38 | _struct bool `codec:",toarray"` //nolint 39 | FormatName string `codec:"format_name"` 40 | Version Version `codec:"vers"` 41 | Type MessageType `codec:"type"` 42 | Ephemeral []byte `codec:"ephemeral"` 43 | SenderSecretbox []byte `codec:"sendersecretbox"` 44 | Receivers []receiverKeys `codec:"rcvrs"` 45 | } 46 | 47 | // encryptionBlockV1 contains a block of encrypted data. It contains 48 | // the ciphertext, and any necessary authentication Tags. 49 | type encryptionBlockV1 struct { 50 | _struct bool `codec:",toarray"` //nolint 51 | HashAuthenticators []payloadAuthenticator `codec:"authenticators"` 52 | PayloadCiphertext []byte `codec:"ctext"` 53 | } 54 | 55 | // encryptionBlockV2 is encryptionBlockV1, but with a flag signifying 56 | // whether or not this is the final packet. 57 | type encryptionBlockV2 struct { 58 | encryptionBlockV1 59 | IsFinal bool `codec:"final"` 60 | } 61 | 62 | // Make *encryptionBlockV2 implement codec.Selfer to encode IsFinal 63 | // first, to preserve the behavior noticed in this issue: 64 | // https://github.com/keybase/saltpack/pull/43 . 65 | 66 | var _ codec.Selfer = (*encryptionBlockV2)(nil) 67 | 68 | func (b *encryptionBlockV2) CodecEncodeSelf(e *codec.Encoder) { 69 | e.MustEncode([]interface{}{ 70 | b.IsFinal, 71 | b.HashAuthenticators, 72 | b.PayloadCiphertext, 73 | }) 74 | } 75 | 76 | func (b *encryptionBlockV2) CodecDecodeSelf(d *codec.Decoder) { 77 | d.MustDecode([]interface{}{ 78 | &b.IsFinal, 79 | &b.HashAuthenticators, 80 | &b.PayloadCiphertext, 81 | }) 82 | } 83 | 84 | func (h *EncryptionHeader) validate(versionValidator func(Version) error) error { 85 | if h.Type != MessageTypeEncryption { 86 | return ErrWrongMessageType{MessageTypeEncryption, h.Type} 87 | } 88 | return versionValidator(h.Version) 89 | } 90 | 91 | // The SigncryptionHeader has exactly the same structure as the 92 | // EncryptionHeader, though the byte slices represent different types of keys. 93 | type SigncryptionHeader EncryptionHeader 94 | 95 | // signcryptionBlock contains a block of signed and encrypted data. 96 | type signcryptionBlock struct { 97 | _struct bool `codec:",toarray"` //nolint 98 | PayloadCiphertext []byte `codec:"ctext"` 99 | IsFinal bool `codec:"final"` 100 | } 101 | 102 | func (h *SigncryptionHeader) validate() error { 103 | if h.Type != MessageTypeSigncryption { 104 | return ErrWrongMessageType{MessageTypeSigncryption, h.Type} 105 | } 106 | if h.Version.Major != Version2().Major { 107 | return ErrBadVersion{h.Version} 108 | } 109 | return nil 110 | } 111 | 112 | // SignatureHeader is the first packet in a signed message. 113 | type SignatureHeader struct { 114 | _struct bool `codec:",toarray"` //nolint 115 | FormatName string `codec:"format_name"` 116 | Version Version `codec:"vers"` 117 | Type MessageType `codec:"type"` 118 | SenderPublic []byte `codec:"sender_public"` 119 | Nonce []byte `codec:"nonce"` 120 | } 121 | 122 | func newSignatureHeader(version Version, sender SigningPublicKey, msgType MessageType) (*SignatureHeader, error) { 123 | if sender == nil { 124 | return nil, ErrInvalidParameter{message: "no public signing key provided"} 125 | } 126 | nonce, err := newSigNonce() 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | header := &SignatureHeader{ 132 | FormatName: FormatName, 133 | Version: version, 134 | Type: msgType, 135 | SenderPublic: sender.ToKID(), 136 | Nonce: nonce[:], 137 | } 138 | 139 | return header, nil 140 | } 141 | 142 | func (h *SignatureHeader) validate(versionValidator VersionValidator, msgType MessageType) error { 143 | if err := versionValidator(h.Version); err != nil { 144 | return err 145 | } 146 | 147 | if h.Type != msgType { 148 | return ErrWrongMessageType{ 149 | Wanted: msgType, 150 | Received: h.Type, 151 | } 152 | } 153 | 154 | if msgType != MessageTypeAttachedSignature && msgType != MessageTypeDetachedSignature { 155 | return ErrInvalidParameter{message: fmt.Sprintf("signature header must be MessageTypeAttachedSignature or MessageTypeDetachedSignature, not %d", msgType)} 156 | } 157 | 158 | return nil 159 | } 160 | 161 | // signatureBlockV1 contains a block of signed data. 162 | type signatureBlockV1 struct { 163 | _struct bool `codec:",toarray"` //nolint 164 | Signature []byte `codec:"signature"` 165 | PayloadChunk []byte `codec:"payload_chunk"` 166 | } 167 | 168 | // signatureBlockV2 is signatureBlockV1, but with a flag signifying 169 | // whether or not this is the final packet. 170 | type signatureBlockV2 struct { 171 | signatureBlockV1 172 | IsFinal bool `codec:"final"` 173 | } 174 | 175 | // Make *signatureBlockV2 implement codec.Selfer to encode IsFinal 176 | // first, to preserve the behavior noticed in this issue: 177 | // https://github.com/keybase/saltpack/pull/43 . 178 | 179 | var _ codec.Selfer = (*signatureBlockV2)(nil) 180 | 181 | func (b *signatureBlockV2) CodecEncodeSelf(e *codec.Encoder) { 182 | e.MustEncode([]interface{}{ 183 | b.IsFinal, 184 | b.Signature, 185 | b.PayloadChunk, 186 | }) 187 | } 188 | 189 | func (b *signatureBlockV2) CodecDecodeSelf(d *codec.Decoder) { 190 | d.MustDecode([]interface{}{ 191 | &b.IsFinal, 192 | &b.Signature, 193 | &b.PayloadChunk, 194 | }) 195 | } 196 | -------------------------------------------------------------------------------- /specs/saltpack_signing_v1.md: -------------------------------------------------------------------------------- 1 | **This document specifies VERSION 1 of the saltpack signing format. This is a 2 | legacy reference, for clients that issued version 1 messages in the past and 3 | need to maintain read support. All new saltpack signing messages should use 4 | VERSION 2.** 5 | 6 | # Saltpack Binary Signing Format [version 1] 7 | 8 | As with the encryption format, we want our signing format to have some 9 | properties on top of a standard NaCl signature: 10 | - Streaming. We want to be able to verify a message of any size, without 11 | fitting the whole thing in RAM, and without requiring a second pass to output 12 | attached plaintext. But we should only ever output verified data. 13 | - Abuse resistance. Alice might use the same signing key for many applications 14 | besides saltpack. Mallory (an attacker) could [try to trick Alice into 15 | signing 16 | messages](https://blog.sandstorm.io/news/2015-05-01-is-that-ascii-or-protobuf.html) 17 | that are meaningful to other applications. Alice should avoid signing bytes 18 | that Mallory could have chosen. 19 | 20 | ## Design 21 | 22 | We define two signing formats: one attached and one detached. The attached 23 | format will have a header packet and payload packets similar to the encryption 24 | format, with an empty packet at the end of the message, and an incrementing 25 | counter to prevent reordering. The detached format will contain just a header 26 | packet, with no payload. 27 | 28 | Both formats will hash a random nonce along with the plaintext, and then sign 29 | the resulting hash. This nonce serves two purposes. First, in the attached 30 | format, it prevents an attacker from swapping in payload packets from other 31 | messages. Second, in both formats, it helps us avoid signing bytes that an 32 | attacker could predict in advance. 33 | 34 | ## Attached Implementation 35 | 36 | When encoding strings, byte arrays, or arrays, pick the MessagePack 37 | encoding that will use the fewest number of bytes. 38 | 39 | An attached signature is a header packet, followed by any number of non-empty 40 | payload packets, followed by an empty payload packet. An attached signing 41 | header packet is a MessagePack array that looks like this: 42 | 43 | ``` 44 | [ 45 | format name, 46 | version, 47 | mode, 48 | sender public key, 49 | nonce, 50 | ] 51 | ``` 52 | 53 | - **format name** is the string "saltpack". 54 | - **version** is a list of the major and minor versions, currently `[1, 0]`, both encoded as 55 | [positive fixnums](https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family). 56 | - **mode** is the number 1, for attached signing, encoded as a 57 | [positive fixnum](https://github.com/msgpack/msgpack/blob/master/spec.md#int-format-family). 58 | (0 is encryption, and 2 is detached signing.) 59 | - **sender public key** is the sender's long-term NaCl signing public key, 32 bytes. 60 | - **nonce** is 32 random bytes. 61 | 62 | As in the encryption spec, the header packet is serialized into a MessagePack 63 | `array` object, hashed with SHA512 to produce the **header hash**, and then 64 | serialized *again* into a MessagePack `bin` object. 65 | 66 | Payload packets are MessagePack arrays that look like this: 67 | 68 | ``` 69 | [ 70 | signature, 71 | payload chunk, 72 | ] 73 | ``` 74 | 75 | - **signature** is a detached NaCl signature, 64 bytes. 76 | - **payload chunk** is a chunk of the plaintext bytes, max size 1 MiB (= 2^20 77 | bytes). 78 | 79 | To make each signature, the sender first takes the SHA512 hash of the 80 | concatenation of three values: 81 | - the **header hash** from above 82 | - the packet sequence number, as a 64-bit big-endian unsigned integer, where 83 | the first payload packet is zero 84 | - the **payload chunk** 85 | 86 | The sender then signs the concatenation of two values: 87 | - `"saltpack attached signature\0"` 88 | - the SHA512 hash above 89 | 90 | As in the encryption spec, after encrypting the entire message, the sender adds 91 | an extra payload packet with an empty payload to signify the end. If a message 92 | doesn't end with an empty payload packet, the receiving client should report an 93 | error that the message has been truncated. 94 | 95 | ## Detached Implementation 96 | 97 | A detached signature header packet is equivalent to an attached signature 98 | header packet with a different mode (2 instead of 1). As above it's 99 | twice-encoded, with the **header hash** computed after the first encoding. A 100 | detached header is followed by a single MessagePack `bin` object, containing 101 | the 64-byte detached NaCl signature. 102 | 103 | ``` 104 | [ 105 | format name, 106 | version, 107 | mode, 108 | sender public key, 109 | nonce, 110 | ] 111 | 112 | signature 113 | ``` 114 | 115 | To make the signature, the sender first takes the SHA512 hash of the 116 | concatenation of two values:: 117 | - the **header hash** from above 118 | - the entire plaintext 119 | 120 | The sender then signs the concatenation of two values: 121 | - `"saltpack detached signature\0"` 122 | - the SHA512 hash above 123 | 124 | ## Examples 125 | 126 | An attached signature: 127 | 128 | ```yaml 129 | # header packet (on the wire, this is double-encoded) 130 | [ 131 | # format name 132 | "saltpack", 133 | # major and minor version 134 | [1, 0], 135 | # mode (1 = attached signing) 136 | 1, 137 | # sender public key 138 | 033e2d505c9ec79fb16fb40f34d743d5805dd36c844741420671fb57df24ed93, 139 | # nonce 140 | 22737333c279555e6e33a79b1ba06cab47d7689d65b43615393d6d607f1d7a52, 141 | ] 142 | 143 | # payload packet 144 | [ 145 | # signature 146 | bba4e5dff8cc6cea88e1d505d1f9a716ca0e79a142e26896493daf82733b4e220c555a0941e52673c25a384f334e0ccdcb62f89a4f01d13f0cb53961f0f4cc00, 147 | # payload chunk 148 | "Yea, though I walk through the valley of the shadow of death, I will fear no evil: for thou art with me; thy rod and thy staff they comfort me.", 149 | ] 150 | 151 | # empty payload packet 152 | [ 153 | # signature 154 | 3a83ebf3fcc2dd30b8c148cd0097ecc93d265e24e83798d28a1370ef54fc8933a9aa56b7118d147cda2ab2c83b378b1b2104e5c6f2320313fc54d173584b0706, 155 | # empty payload chunk (a zero-length byte string) 156 | "", 157 | ] 158 | ``` 159 | 160 | A detached signature: 161 | 162 | ```yaml 163 | # header packet (on the wire, this is double-encoded) 164 | [ 165 | # format name 166 | "saltpack", 167 | # major and minor version 168 | [1, 0], 169 | # mode (2 = detached signing) 170 | 2, 171 | # sender public key 172 | 8a732edded5a23036c4c8caca6a04321f007b2e8b60cab975950a17b4c02ca76, 173 | # nonce 174 | 38d01af91e8cd9e6a79f19ea55733fea9778be327170d4fd8ed2074d4be0b784, 175 | ] 176 | 177 | # signature 178 | 6628f12374ce7c2a55d1c864e71206296e380586c9e61c3aa3ac1b5f11674bd53b895705183ff54d00fdeb5534b412569f58cb22dc6b3673b9a265e3bffe470d 179 | ``` 180 | -------------------------------------------------------------------------------- /sign_stream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "crypto/sha512" 9 | "fmt" 10 | "hash" 11 | "io" 12 | ) 13 | 14 | type signAttachedStream struct { 15 | version Version 16 | headerHash headerHash 17 | encoder encoder 18 | buffer bytes.Buffer 19 | seqno packetSeqno 20 | secretKey SigningSecretKey 21 | } 22 | 23 | func newSignAttachedStream(version Version, w io.Writer, signer SigningSecretKey) (*signAttachedStream, error) { 24 | if signer == nil { 25 | return nil, ErrInvalidParameter{message: "no signing key provided"} 26 | } 27 | 28 | header, err := newSignatureHeader(version, signer.GetPublicKey(), MessageTypeAttachedSignature) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | // Encode the header bytes. 34 | headerBytes, err := encodeToBytes(header) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | // Compute the header hash. 40 | headerHash := hashHeader(headerBytes) 41 | 42 | // Create the attached stream object. 43 | stream := &signAttachedStream{ 44 | version: version, 45 | headerHash: headerHash, 46 | encoder: newEncoder(w), 47 | secretKey: signer, 48 | } 49 | 50 | // Double encode the header bytes onto the wire. 51 | err = stream.encoder.Encode(headerBytes) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return stream, nil 57 | } 58 | 59 | func (s *signAttachedStream) Write(p []byte) (int, error) { 60 | n, err := s.buffer.Write(p) 61 | if err != nil { 62 | return 0, err 63 | } 64 | 65 | // If s.buffer.Len() == signatureBlockSize, we don't want to 66 | // write it out just yet, since for V2 we need to be sure this 67 | // isn't the last block. 68 | for s.buffer.Len() > signatureBlockSize { 69 | if err := s.signBlock(false); err != nil { 70 | return 0, err 71 | } 72 | } 73 | 74 | return n, nil 75 | } 76 | 77 | func (s *signAttachedStream) Close() error { 78 | switch s.version { 79 | case Version1(): 80 | if s.buffer.Len() > 0 { 81 | if err := s.signBlock(false); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | if s.buffer.Len() > 0 { 87 | panic(fmt.Sprintf("s.buffer.Len()=%d > 0", s.buffer.Len())) 88 | } 89 | 90 | return s.signBlock(true) 91 | 92 | case Version2(): 93 | if err := s.signBlock(true); err != nil { 94 | return err 95 | } 96 | 97 | if s.buffer.Len() > 0 { 98 | panic(fmt.Sprintf("s.buffer.Len()=%d > 0", s.buffer.Len())) 99 | } 100 | 101 | return nil 102 | 103 | default: 104 | panic(ErrBadVersion{s.version}) 105 | } 106 | } 107 | 108 | func makeSignatureBlock(version Version, sig, chunk []byte, isFinal bool) interface{} { 109 | sbV1 := signatureBlockV1{ 110 | Signature: sig, 111 | PayloadChunk: chunk, 112 | } 113 | switch version { 114 | case Version1(): 115 | return sbV1 116 | case Version2(): 117 | return signatureBlockV2{ 118 | signatureBlockV1: sbV1, 119 | IsFinal: isFinal, 120 | } 121 | default: 122 | panic(ErrBadVersion{version}) 123 | } 124 | } 125 | 126 | func checkSignBlockRead(version Version, isFinal bool, blockSize, chunkLen, bufLen int) { 127 | die := func() { 128 | panic(fmt.Errorf("invalid signBlock read state: version=%s, isFinal=%t, chunkLen=%d, bufLen=%d", version, isFinal, chunkLen, bufLen)) 129 | } 130 | 131 | // We shouldn't read more than a full block's worth. 132 | if chunkLen > blockSize { 133 | die() 134 | } 135 | 136 | // If we read less than a full block's worth, then we 137 | // shouldn't have anything left in the buffer. 138 | if chunkLen < blockSize && bufLen > 0 { 139 | die() 140 | } 141 | 142 | switch version { 143 | case Version1(): 144 | // isFinal must be equivalent to chunkLen being 0 145 | // (which, by the above, implies that bufLen == 0). 146 | if isFinal != (chunkLen == 0) { 147 | die() 148 | } 149 | 150 | case Version2(): 151 | // If isFinal, then chunkLen can be any number, 152 | // but bufLen must be 0. 153 | if isFinal && (bufLen != 0) { 154 | die() 155 | } 156 | 157 | default: 158 | panic(ErrBadVersion{version}) 159 | } 160 | } 161 | 162 | func (s *signAttachedStream) signBlock(isFinal bool) error { 163 | // NOTE: chunk is a slice into s.buffer's buffer, so make sure 164 | // not to stash it anywhere. 165 | chunk := s.buffer.Next(signatureBlockSize) 166 | checkSignBlockRead(s.version, isFinal, signatureBlockSize, len(chunk), s.buffer.Len()) 167 | 168 | sig, err := s.computeSig(chunk, s.seqno, isFinal) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | assertEncodedChunkState(s.version, chunk, 0, uint64(s.seqno), isFinal) 174 | 175 | sBlock := makeSignatureBlock(s.version, sig, chunk, isFinal) 176 | if err := s.encoder.Encode(sBlock); err != nil { 177 | return err 178 | } 179 | 180 | s.seqno++ 181 | return nil 182 | } 183 | 184 | func (s *signAttachedStream) computeSig(payloadChunk []byte, seqno packetSeqno, isFinal bool) ([]byte, error) { 185 | return s.secretKey.Sign(attachedSignatureInput(s.version, s.headerHash, payloadChunk, seqno, isFinal)) 186 | } 187 | 188 | type signDetachedStream struct { 189 | encoder encoder 190 | secretKey SigningSecretKey 191 | hasher hash.Hash 192 | } 193 | 194 | func newSignDetachedStream(version Version, w io.Writer, signer SigningSecretKey) (*signDetachedStream, error) { 195 | if signer == nil { 196 | return nil, ErrInvalidParameter{message: "no signing key provided"} 197 | } 198 | 199 | header, err := newSignatureHeader(version, signer.GetPublicKey(), MessageTypeDetachedSignature) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | // Encode the header bytes. 205 | headerBytes, err := encodeToBytes(header) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | // Compute the header hash. 211 | headerHash := hashHeader(headerBytes) 212 | 213 | // Create the detached stream object. 214 | stream := &signDetachedStream{ 215 | encoder: newEncoder(w), 216 | secretKey: signer, 217 | hasher: sha512.New(), 218 | } 219 | 220 | // Double encode the header bytes onto the wire. 221 | err = stream.encoder.Encode(headerBytes) 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | // Start off the message digest with the header hash. Subsequent calls to 227 | // Write() will push message bytes into this digest. 228 | _, err = stream.hasher.Write(headerHash[:]) 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | return stream, nil 234 | } 235 | 236 | func (s *signDetachedStream) Write(p []byte) (int, error) { 237 | return s.hasher.Write(p) 238 | } 239 | 240 | func (s *signDetachedStream) Close() error { 241 | signature, err := s.secretKey.Sign(detachedSignatureInputFromHash(s.hasher.Sum(nil))) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | return s.encoder.Encode(signature) 247 | } 248 | -------------------------------------------------------------------------------- /encoding/basex/stream.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package basex 5 | 6 | import "io" 7 | 8 | // Much of this code is adopted from Go's encoding/base64 9 | 10 | // EncodeToString returns the baseX encoding of src. 11 | func (enc *Encoding) EncodeToString(src []byte) string { 12 | buf := make([]byte, enc.EncodedLen(len(src))) 13 | enc.Encode(buf, src) 14 | return string(buf) 15 | } 16 | 17 | type encoder struct { 18 | err error 19 | enc *Encoding 20 | w io.Writer 21 | buf []byte // buffered data waiting to be encoded 22 | nbuf int // number of bytes in buf 23 | out []byte // output buffer 24 | } 25 | 26 | func (e *encoder) Write(p []byte) (n int, err error) { 27 | if e.err != nil { 28 | return 0, e.err 29 | } 30 | 31 | ibl := e.enc.base256BlockLen 32 | obl := e.enc.baseXBlockLen 33 | 34 | // Leading fringe. 35 | if e.nbuf > 0 { 36 | var i int 37 | for i = 0; i < len(p) && e.nbuf < ibl; i++ { 38 | e.buf[e.nbuf] = p[i] 39 | e.nbuf++ 40 | } 41 | n += i 42 | p = p[i:] 43 | if e.nbuf < ibl { 44 | return 45 | } 46 | e.enc.Encode(e.out, e.buf) 47 | if _, e.err = e.w.Write(e.out[:obl]); e.err != nil { 48 | return n, e.err 49 | } 50 | e.nbuf = 0 51 | } 52 | 53 | // Large interior chunks. 54 | for len(p) >= ibl { 55 | nn := len(e.out) / obl * ibl 56 | if nn > len(p) { 57 | nn = len(p) 58 | nn -= nn % ibl 59 | } 60 | e.enc.Encode(e.out, p[:nn]) 61 | if _, e.err = e.w.Write(e.out[0 : nn/ibl*obl]); e.err != nil { 62 | return n, e.err 63 | } 64 | n += nn 65 | p = p[nn:] 66 | } 67 | 68 | // Trailing fringe. 69 | copy(e.buf[0:len(p)], p) 70 | e.nbuf = len(p) 71 | n += len(p) 72 | return 73 | } 74 | 75 | // Close flushes any pending output from the encoder. 76 | // It is an error to call Write after calling Close. 77 | func (e *encoder) Close() error { 78 | // If there's anything left in the buffer, flush it out 79 | if e.err == nil && e.nbuf > 0 { 80 | e.enc.Encode(e.out, e.buf[:e.nbuf]) 81 | _, e.err = e.w.Write(e.out[:e.enc.EncodedLen(e.nbuf)]) 82 | e.nbuf = 0 83 | } 84 | return e.err 85 | } 86 | 87 | // NewEncoder returns a new baseX stream encoder. Data written to 88 | // the returned writer will be encoded using enc and then written to w. 89 | // Encodings operate in enc.baseXBlockLen-byte blocks; when finished 90 | // writing, the caller must Close the returned encoder to flush any 91 | // partially written blocks. 92 | func NewEncoder(enc *Encoding, w io.Writer) io.WriteCloser { 93 | return &encoder{ 94 | enc: enc, 95 | w: w, 96 | buf: make([]byte, enc.base256BlockLen), 97 | out: make([]byte, 128*enc.baseXBlockLen), 98 | } 99 | } 100 | 101 | // DecodeString returns the bytes represented by the baseX string s. 102 | // It uses the liberal decoding strategy, ignoring any non-baseX-characters 103 | func (enc *Encoding) DecodeString(s string) ([]byte, error) { 104 | dbuf := make([]byte, enc.DecodedLen(len(s))) 105 | n, err := enc.Decode(dbuf, []byte(s)) 106 | return dbuf[:n], err 107 | } 108 | 109 | type decoder struct { 110 | err error 111 | enc *Encoding 112 | r io.Reader 113 | out []byte // leftover decoded output 114 | buf []byte // leftover input 115 | nbuf int // the begin pointer of buf above 116 | scratchbuf []byte // a temporary scratch buf, for reuse 117 | } 118 | 119 | func (d *decoder) Read(p []byte) (int, error) { 120 | if d.err != nil { 121 | return 0, d.err 122 | } 123 | 124 | // Use leftover decoded output from last read. 125 | if len(d.out) > 0 { 126 | ret := copy(p, d.out) 127 | d.out = d.out[ret:] 128 | return ret, nil 129 | } 130 | 131 | ibl := d.enc.base256BlockLen 132 | obl := d.enc.baseXBlockLen 133 | 134 | nn := len(p) / ibl * obl 135 | if nn < obl { 136 | nn = obl 137 | } 138 | if nn > len(d.buf) { 139 | nn = len(d.buf) 140 | } 141 | 142 | // Try to read up to the next full block. 143 | for d.nbuf < obl && d.err == nil { 144 | var n int 145 | n, d.err = d.r.Read(d.buf[d.nbuf:nn]) 146 | d.nbuf += n 147 | } 148 | 149 | eof := false 150 | 151 | if d.err == io.EOF { 152 | if d.nbuf == 0 { 153 | return 0, d.err 154 | } 155 | eof = true 156 | d.err = nil 157 | } else if d.err != nil { 158 | return 0, d.err 159 | } 160 | 161 | // The num bytes to decode should be along obl-aligned boundaries, unless 162 | // we're at the end of file. 163 | numBytesToDecode := d.nbuf 164 | if !eof { 165 | numBytesToDecode = numBytesToDecode / obl * obl 166 | } 167 | numBytesToOutput := d.enc.DecodedLen(numBytesToDecode) 168 | 169 | var ret int 170 | 171 | // If we have too many bytes for the given buffer, we can buffer 172 | // the rest internally 173 | if numBytesToOutput > len(p) { 174 | var n int 175 | n, d.err = d.enc.Decode(d.scratchbuf, d.buf[:numBytesToDecode]) 176 | d.out = d.scratchbuf[:n] 177 | ret = copy(p, d.out) 178 | d.out = d.out[ret:] 179 | } else { 180 | ret, d.err = d.enc.Decode(p, d.buf[:numBytesToDecode]) 181 | } 182 | 183 | // Shift the bytes in d.buf over from [numBytesToDecode:] to the start of the array 184 | d.nbuf -= numBytesToDecode 185 | copy(d.buf[0:d.nbuf], d.buf[numBytesToDecode:numBytesToDecode+d.nbuf]) 186 | 187 | if ret == 0 && d.err == nil && len(p) != 0 { 188 | return 0, io.EOF 189 | } 190 | 191 | return ret, d.err 192 | } 193 | 194 | type filteringReader struct { 195 | wrapped io.Reader 196 | enc *Encoding 197 | nRead int 198 | } 199 | 200 | func (r *filteringReader) Read(p []byte) (int, error) { 201 | n, err := r.wrapped.Read(p) 202 | for n > 0 { 203 | offset := 0 204 | for i, b := range p[:n] { 205 | typ := r.enc.getByteType(b) 206 | if typ == invalidByteType { 207 | // TODO: Return n, i.e. partial results? 208 | return 0, CorruptInputError(r.nRead) 209 | } 210 | r.nRead++ 211 | if typ == skipByteType { 212 | continue 213 | } 214 | 215 | // We want this byte. We only need to rewrite it if 216 | // offset is behind i (otherwise we'd just be writing the 217 | // same byte again, over itself). 218 | if i != offset { 219 | p[offset] = b 220 | } 221 | offset++ 222 | } 223 | if offset > 0 { 224 | return offset, err 225 | } 226 | // Previous buffer entirely whitespace, read again 227 | n, err = r.wrapped.Read(p) 228 | } 229 | return n, err 230 | } 231 | 232 | // NewDecoder constructs a new baseX stream decoder. 233 | func NewDecoder(enc *Encoding, r io.Reader) io.Reader { 234 | return newDecoder(enc, r) 235 | } 236 | 237 | func newDecoder(enc *Encoding, r io.Reader) io.Reader { 238 | if enc.hasSkipBytes() { 239 | r = &filteringReader{r, enc, 0} 240 | } 241 | return &decoder{ 242 | enc: enc, 243 | r: r, 244 | buf: make([]byte, 8192*enc.base256BlockLen), 245 | scratchbuf: make([]byte, 8192*enc.baseXBlockLen), 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /rand_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | //go:build go1.10 5 | // +build go1.10 6 | 7 | package saltpack 8 | 9 | import ( 10 | "bytes" 11 | cryptorand "crypto/rand" 12 | "encoding/binary" 13 | "flag" 14 | "fmt" 15 | "io" 16 | mathrand "math/rand" 17 | "runtime" 18 | "sync" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestCSPRNGUint32(t *testing.T) { 26 | var buf [4]byte 27 | binary.BigEndian.PutUint32(buf[:], 0xdeadbeef) 28 | r := bytes.NewReader(buf[:]) 29 | n, err := csprngUint32(r) 30 | require.NoError(t, err) 31 | require.Equal(t, uint32(0xdeadbeef), n) 32 | } 33 | 34 | func TestCSPRNGUint32Error(t *testing.T) { 35 | var buf [3]byte 36 | r := bytes.NewReader(buf[:]) 37 | _, err := csprngUint32(r) 38 | require.Equal(t, io.ErrUnexpectedEOF, err) 39 | } 40 | 41 | func TestCSPRNGUint32nFastPath(t *testing.T) { 42 | var buf [4]byte 43 | binary.BigEndian.PutUint32(buf[:], 0xdeadbeef) 44 | r := bytes.NewReader(buf[:]) 45 | n, err := csprngUint32n(r, 100) 46 | require.NoError(t, err) 47 | // (0xdeadbeef * 100) % 0x100000000 = 422566844 >= 96, 48 | // 49 | // so the first sample is accepted, and the quotient 50 | // 51 | // (0xdeadbeef * 100) / 0x100000000 = 86 52 | // 53 | // is returned. 54 | require.Equal(t, uint32(86), n) 55 | require.Equal(t, 0, r.Len()) 56 | } 57 | 58 | func TestCSPRNGUint32nSlowPath(t *testing.T) { 59 | var buf [8]byte 60 | binary.BigEndian.PutUint32(buf[:], 0xdeadbeef+692989) 61 | binary.BigEndian.PutUint32(buf[4:], 0xdeadbeef) 62 | r := bytes.NewReader(buf[:]) 63 | n, err := csprngUint32n(r, 100) 64 | require.NoError(t, err) 65 | // ((0xdeadbeef + 692989) * 100) % 0x100000000 = 48 < 96, 66 | // 67 | // so the first sample is rejected, and the second sample is 68 | // accepted (by the same reasoning as above). 69 | require.Equal(t, uint32(86), n) 70 | require.Equal(t, 0, r.Len()) 71 | } 72 | 73 | // A flag controlling whether to run long-running tests that is false 74 | // by default. 75 | var long = flag.Bool("long", false, "whether to run long-running tests") 76 | 77 | func testCSPRNGUint32nUniform(t *testing.T, n uint32) { 78 | if !*long { 79 | t.Skip() 80 | } 81 | 82 | // Split the 32-bit range into roughly equal ranges for each 83 | // worker and have each worker keep a count of how many times 84 | // each number is returned. 85 | 86 | workerCount := runtime.NumCPU() 87 | workerBuckets := make([][]uint64, workerCount) 88 | for i := 0; i < workerCount; i++ { 89 | workerBuckets[i] = make([]uint64, n) 90 | } 91 | 92 | var w sync.WaitGroup 93 | w.Add(workerCount) 94 | 95 | //nolint:gosec // workerCount is a small test parameter, conversion is safe 96 | rangeSize := uint64(1<<32) / uint64(workerCount) 97 | 98 | for i := 0; i < workerCount; i++ { 99 | // Capture range variable. 100 | i := i 101 | //nolint:gosec // i is bounded by workerCount, conversion is safe 102 | start := uint64(i) * rangeSize 103 | //nolint:gosec // i is bounded by workerCount, conversion is safe 104 | end := uint64(i+1) * rangeSize 105 | if end > (1 << 32) { 106 | end = 1 << 32 107 | } 108 | go func(start, end uint64, bucket *[]uint64) { 109 | defer w.Done() 110 | 111 | var buf [4]byte 112 | r := bytes.NewReader(buf[:]) 113 | for j := start; j < end; j++ { 114 | if j%10000000 == 0 { 115 | // Use fmt.Printf instead of 116 | // t.Log so that it prints as 117 | // the test is running. 118 | fmt.Printf("worker %d/%d: %.2f%% done\n", i+1, workerCount, float64(j-start)*100/float64(end-start)) 119 | } 120 | 121 | //nolint:gosec // j is bounded by test data range, conversion is safe 122 | binary.BigEndian.PutUint32(buf[:], uint32(j)) 123 | _, err := r.Seek(0, io.SeekStart) 124 | require.NoError(t, err) 125 | m, err := csprngUint32n(r, n) 126 | if err != nil { 127 | require.Equal(t, io.EOF, err) 128 | } else { 129 | (*bucket)[m]++ 130 | } 131 | } 132 | }(start, end, &workerBuckets[i]) 133 | } 134 | 135 | w.Wait() 136 | 137 | // Then add together all the counts. Each number should appear 138 | // exactly floor(2³²/n) times. 139 | 140 | buckets := make([]uint64, n) 141 | for i := uint32(0); i < n; i++ { 142 | for j := 0; j < workerCount; j++ { 143 | buckets[i] += workerBuckets[j][i] 144 | } 145 | } 146 | 147 | for i := uint32(0); i < n; i++ { 148 | assert.Equal(t, (1<<32)/uint64(n), buckets[i], "i=%d", i) 149 | } 150 | } 151 | 152 | func TestCSPRNGUint32nUniform(t *testing.T) { 153 | for _, n := range []uint32{ 154 | 49, // coprime to 2³² 155 | 100, // shares factors with 2³² 156 | 65536, // divides 2³² 157 | } { 158 | // Capture range variable. 159 | n := n 160 | t.Run(fmt.Sprintf("%d", n), func(t *testing.T) { 161 | testCSPRNGUint32nUniform(t, n) 162 | }) 163 | } 164 | } 165 | 166 | type testReaderSource struct { 167 | t *testing.T 168 | r io.Reader 169 | // Stores the bytes read from r for later playback. 170 | read []byte 171 | } 172 | 173 | var _ mathrand.Source = (*testReaderSource)(nil) 174 | 175 | func (s *testReaderSource) Int63() int64 { 176 | randN, err := csprngUint32(s.r) 177 | require.NoError(s.t, err) 178 | 179 | // math/rand.Shuffle calls r.Uint32(), which returns 180 | // uint32(r.src.Int63() >> 31), so we only need to fill in the 181 | // top 32 bits after the sign bit. 182 | n := int64(randN) << 31 183 | 184 | // Assumes that cryptorandUint32 uses big endian. 185 | var buf [4]byte 186 | binary.BigEndian.PutUint32(buf[:], randN) 187 | s.read = append(s.read, buf[:]...) 188 | 189 | return n 190 | } 191 | 192 | func (s testReaderSource) Seed(_ int64) { 193 | s.t.Fatal("testReaderSource.Seed() called unexpectedly") 194 | } 195 | 196 | // testCSPRNGShuffle tests that csprngShuffle exactly matches 197 | // math/rand.Shuffle for the given size, which must be less than 198 | // 2³¹. This is a robust test, since go's backwards compatibility 199 | // guarantee also applies to the behavior of math/rand.Rand for a 200 | // given seed. 201 | func testCSPRNGShuffle(t *testing.T, size int) { 202 | var input []int 203 | for i := 0; i < size; i++ { 204 | input = append(input, size) 205 | } 206 | 207 | expectedOutput := make([]int, len(input)) 208 | output := make([]int, len(input)) 209 | 210 | copy(expectedOutput, input) 211 | copy(output, input) 212 | 213 | sourceExpected := testReaderSource{t, cryptorand.Reader, nil} 214 | //nolint:gosec // using math/rand with crypto/rand source for testing 215 | rnd := mathrand.New(&sourceExpected) 216 | rnd.Shuffle(len(expectedOutput), func(i, j int) { 217 | expectedOutput[i], expectedOutput[j] = expectedOutput[j], expectedOutput[i] 218 | }) 219 | 220 | r := bytes.NewReader(sourceExpected.read) 221 | err := csprngShuffle(r, len(output), func(i, j int) { 222 | output[i], output[j] = output[j], output[i] 223 | }) 224 | require.NoError(t, err) 225 | 226 | require.Equal(t, expectedOutput, output) 227 | require.Equal(t, 0, r.Len()) 228 | } 229 | 230 | func TestCSPRNGShuffle(t *testing.T) { 231 | for _, size := range []int{ 232 | 100, 233 | 16807, // 7⁵ 234 | 65536, 235 | 100000, 236 | } { 237 | // Capture range variable. 238 | size := size 239 | t.Run(fmt.Sprintf("%d", size), func(t *testing.T) { 240 | testCSPRNGShuffle(t, size) 241 | }) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "reflect" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestComputePayloadAuthenticator(t *testing.T) { 14 | macKeys := []macKey{{0x01}, {0x02}} 15 | payloadHashes := []payloadHash{{0x03}, {0x04}} 16 | 17 | expectedAuthenticators := []payloadAuthenticator{ 18 | {0xf, 0x2f, 0x81, 0xfb, 0xdb, 0x34, 0xc5, 0x61, 0x86, 0xfa, 0x72, 0x70, 0xd1, 0xd, 0xe5, 0x9f, 0x3d, 0x7e, 0x39, 0xcf, 0x9f, 0xa1, 0xf9, 0x9b, 0xc4, 0x38, 0x70, 0xa, 0x28, 0x5f, 0xeb, 0xd3}, 19 | {0x2d, 0x7, 0x95, 0x64, 0xfa, 0xaf, 0xce, 0xde, 0x7a, 0x85, 0xea, 0xce, 0x78, 0xec, 0x71, 0xf, 0x84, 0x17, 0x9a, 0x32, 0x44, 0x2b, 0xb5, 0x4, 0xe9, 0x92, 0x28, 0x98, 0x4f, 0xfe, 0x9b, 0x5b}, 20 | {0x16, 0xbd, 0xdb, 0xd, 0x5d, 0x71, 0xe2, 0xee, 0x58, 0x5a, 0x32, 0xcb, 0x27, 0xd4, 0x1e, 0x42, 0xff, 0xb5, 0xc3, 0x98, 0x81, 0x1c, 0xbd, 0x5e, 0x43, 0x9a, 0x4d, 0x55, 0xa7, 0xa5, 0xd1, 0x2b}, 21 | {0x7c, 0xcd, 0x4f, 0xe3, 0xf5, 0xf6, 0x54, 0x7d, 0x65, 0x97, 0x90, 0x22, 0x9, 0xfb, 0x46, 0x69, 0xcd, 0x7a, 0x70, 0x9a, 0xa2, 0x5e, 0x1d, 0xa5, 0xe4, 0xc1, 0xf5, 0x14, 0x67, 0x55, 0xd4, 0xd8}, 22 | } 23 | 24 | i := 0 25 | for _, macKey := range macKeys { 26 | for _, payloadHash := range payloadHashes { 27 | authenticator := computePayloadAuthenticator(macKey, payloadHash) 28 | if !authenticator.Equal(expectedAuthenticators[i]) { 29 | t.Errorf("Got %#v, expected %#v", authenticator, expectedAuthenticators[i]) 30 | } 31 | i++ 32 | } 33 | } 34 | } 35 | 36 | func runTestOverVersions(t *testing.T, f func(t *testing.T, version Version)) { 37 | for _, version := range KnownVersions() { 38 | version := version // capture range variable. 39 | t.Run(version.String(), func(t *testing.T) { 40 | f(t, version) 41 | }) 42 | } 43 | } 44 | 45 | // runTestsOverVersions runs the given list of test functions over all 46 | // versions to test. prefix should be the common prefix for all the 47 | // test function names, and the names of the subtest will be taken to 48 | // be the strings after that prefix. Example use: 49 | // 50 | // func TestFoo(t *testing.T) { 51 | // tests := []func(*testing.T, Version){ 52 | // testFooBar1, 53 | // testFooBar2, 54 | // testFooBar3, 55 | // ... 56 | // } 57 | // runTestsOverVersions(t, "testFoo", tests) 58 | // } 59 | func runTestsOverVersions(t *testing.T, prefix string, fs []func(t *testing.T, ver Version)) { 60 | for _, f := range fs { 61 | f := f // capture range variable. 62 | name := runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 63 | i := strings.LastIndex(name, prefix) 64 | if i >= 0 { 65 | i += len(prefix) 66 | } else { 67 | i = 0 68 | } 69 | name = name[i:] 70 | t.Run(name, func(t *testing.T) { 71 | runTestOverVersions(t, f) 72 | }) 73 | } 74 | } 75 | 76 | // Due to the specifics of Curve25519 (see https://cr.yp.to/ecdh.html ), 77 | // the lower three bits of boxSecretKey.key[0] don't suffice to 78 | // distinguish two secret keys. 79 | 80 | var secret1 = boxSecretKey{ 81 | key: RawBoxKey{0x08}, 82 | } 83 | 84 | var secret2 = boxSecretKey{ 85 | key: RawBoxKey{0x10}, 86 | } 87 | 88 | var eSecret1 = boxSecretKey{ 89 | key: RawBoxKey{0x18}, 90 | } 91 | 92 | var eSecret2 = boxSecretKey{ 93 | key: RawBoxKey{0x20}, 94 | } 95 | 96 | var public1 = boxPublicKey{ 97 | key: RawBoxKey{0x5}, 98 | } 99 | 100 | var public2 = boxPublicKey{ 101 | key: RawBoxKey{0x6}, 102 | } 103 | 104 | var constHeaderHash = headerHash{0x7} 105 | 106 | func TestComputeMACKeySenderV1(t *testing.T) { 107 | macKey1 := computeMACKeySender(Version1(), 0, secret1, eSecret1, public1, constHeaderHash) 108 | macKey2 := computeMACKeySender(Version1(), 1, secret1, eSecret1, public1, constHeaderHash) 109 | macKey3 := computeMACKeySender(Version1(), 0, secret2, eSecret1, public1, constHeaderHash) 110 | macKey4 := computeMACKeySender(Version1(), 0, secret1, eSecret2, public1, constHeaderHash) 111 | macKey5 := computeMACKeySender(Version1(), 0, secret1, eSecret1, public2, constHeaderHash) 112 | 113 | // The V1 MAC key doesn't depend on the index; this is fixed 114 | // in V2. 115 | if macKey2 != macKey1 { 116 | t.Errorf("macKey2 == %v != macKey1 == %v unexpectedly", macKey2, macKey1) 117 | } 118 | 119 | if macKey3 == macKey1 { 120 | t.Errorf("macKey3 == macKey1 == %v unexpectedly", macKey1) 121 | } 122 | 123 | // The V1 MAC key doesn't depend on the ephemeral keypair; this is 124 | // fixed in V2. 125 | if macKey4 != macKey1 { 126 | t.Errorf("macKey4 == %v != macKey1 == %v unexpectedly", macKey4, macKey1) 127 | } 128 | 129 | if macKey5 == macKey1 { 130 | t.Errorf("macKey5 == macKey1 == %v unexpectedly", macKey1) 131 | } 132 | } 133 | 134 | func TestComputeMACKeySenderV2(t *testing.T) { 135 | macKey1 := computeMACKeySender(Version2(), 0, secret1, eSecret1, public1, constHeaderHash) 136 | macKey2 := computeMACKeySender(Version2(), 1, secret1, eSecret1, public1, constHeaderHash) 137 | macKey3 := computeMACKeySender(Version2(), 0, secret2, eSecret1, public1, constHeaderHash) 138 | macKey4 := computeMACKeySender(Version2(), 0, secret1, eSecret2, public1, constHeaderHash) 139 | macKey5 := computeMACKeySender(Version2(), 0, secret1, eSecret1, public2, constHeaderHash) 140 | 141 | if macKey2 == macKey1 { 142 | t.Errorf("macKey2 == macKey1 == %v unexpectedly", macKey1) 143 | } 144 | 145 | if macKey3 == macKey1 { 146 | t.Errorf("macKey3 == macKey1 == %v unexpectedly", macKey1) 147 | } 148 | 149 | if macKey4 == macKey1 { 150 | t.Errorf("macKey4 == macKey1 == %v unexpectedly", macKey1) 151 | } 152 | 153 | if macKey5 == macKey1 { 154 | t.Errorf("macKey5 == macKey1 == %v unexpectedly", macKey1) 155 | } 156 | } 157 | 158 | func TestComputeMACKeySendersSameRecipientV1(t *testing.T) { 159 | receivers := []BoxPublicKey{public1, public1} 160 | macKeys := computeMACKeysSender(Version1(), secret1, eSecret1, receivers, constHeaderHash) 161 | 162 | if len(macKeys) != 2 { 163 | t.Fatalf("len(macKeys)=%d != 2 unexpectedly", len(macKeys)) 164 | } 165 | 166 | // Identical recipients lead to identical MAC keys in V1; this 167 | // is fixed in V2. 168 | if macKeys[0] != macKeys[1] { 169 | t.Errorf("macKeys[0] = %v != macKeys[1] = %v unexpectedly", macKeys[0], macKeys[1]) 170 | } 171 | } 172 | 173 | func TestComputeMACKeySendersSameRecipientV2(t *testing.T) { 174 | receivers := []BoxPublicKey{public1, public1} 175 | macKeys := computeMACKeysSender(Version2(), secret1, eSecret1, receivers, constHeaderHash) 176 | 177 | if len(macKeys) != 2 { 178 | t.Fatalf("len(macKeys)=%d != 2 unexpectedly", len(macKeys)) 179 | } 180 | 181 | if macKeys[0] == macKeys[1] { 182 | t.Errorf("macKeys[0] == macKeys[1] = %v unexpectedly", macKeys[0]) 183 | } 184 | } 185 | 186 | func testComputeMACKeySenderReceiver(t *testing.T, version Version) { 187 | var index uint64 = 3 188 | senderKey := newBoxKeyNoInsert(t) 189 | eKey := newBoxKeyNoInsert(t) 190 | receiverKey := newBoxKeyNoInsert(t) 191 | 192 | senderMACKey := computeMACKeySender(version, index, senderKey, eKey, receiverKey.GetPublicKey(), constHeaderHash) 193 | receiverMACKey := computeMACKeyReceiver(version, index, receiverKey, senderKey.GetPublicKey(), eKey.GetPublicKey(), constHeaderHash) 194 | if senderMACKey != receiverMACKey { 195 | t.Fatalf("senderMACKey = %v != receiverMACKey = %v", senderMACKey, receiverMACKey) 196 | } 197 | } 198 | 199 | func TestCommon(t *testing.T) { 200 | tests := []func(*testing.T, Version){ 201 | testComputeMACKeySenderReceiver, 202 | } 203 | runTestsOverVersions(t, "test", tests) 204 | } 205 | -------------------------------------------------------------------------------- /chunk_reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | type exampleBlock struct { 18 | // The "encrypted" ciphertext is just the bitwise negation of 19 | // the plaintext. 20 | PayloadCiphertext []byte 21 | Seqno packetSeqno 22 | IsFinal bool 23 | } 24 | 25 | // exampleChunker is an example implementation of chunker that real 26 | // implementations should follow pretty closely. 27 | type exampleChunker struct { 28 | mps *msgpackStream 29 | } 30 | 31 | func newExampleChunker(mps *msgpackStream) exampleChunker { 32 | var headerBytes []byte 33 | _, err := mps.Read(&headerBytes) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | return exampleChunker{mps} 39 | } 40 | 41 | func (c exampleChunker) processBlock(block exampleBlock, seqno packetSeqno) ([]byte, error) { 42 | // A real implementation would check signatures, check MACs, 43 | // decrypt ciphertext, etc. 44 | if seqno != block.Seqno { 45 | return nil, fmt.Errorf("expected seqno %d, got %d", seqno, block.Seqno) 46 | } 47 | 48 | chunk := make([]byte, len(block.PayloadCiphertext)) 49 | for i, b := range block.PayloadCiphertext { 50 | chunk[i] = ^b 51 | } 52 | return chunk, nil 53 | } 54 | 55 | func (c exampleChunker) getNextChunk() ([]byte, error) { 56 | var block exampleBlock 57 | seqno, err := c.mps.Read(&block) 58 | if err != nil { 59 | // An EOF here is unexpected. 60 | if err == io.EOF { 61 | err = io.ErrUnexpectedEOF 62 | } 63 | return nil, err 64 | } 65 | 66 | // If processBlock returns a non-nil error, chunk must be empty. 67 | chunk, err := c.processBlock(block, seqno) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | err = checkDecodedChunkState(Version2(), chunk, block.Seqno, block.IsFinal) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | // There should be nothing else after a final block. 78 | if block.IsFinal { 79 | return chunk, assertEndOfStream(c.mps) 80 | } 81 | 82 | // chunk must be non-empty here; otherwise, chunkReader.Read() 83 | // will panic. 84 | return chunk, nil 85 | } 86 | 87 | func exampleEncode(plaintext []byte) []byte { 88 | buf := bytes.NewBuffer(nil) 89 | encoder := newEncoder(buf) 90 | var headerBytes []byte 91 | // A real implementation would encode the header into 92 | // headerBytes. 93 | err := encoder.Encode(headerBytes) 94 | if err != nil { 95 | panic(err) 96 | } 97 | for i := 0; i < len(plaintext); i++ { 98 | block := exampleBlock{ 99 | PayloadCiphertext: []byte{^plaintext[i]}, 100 | //nolint:gosec // i is a valid slice index, conversion is safe 101 | Seqno: packetSeqno(i + 1), 102 | IsFinal: i == len(plaintext)-1, 103 | } 104 | err := encoder.Encode(block) 105 | if err != nil { 106 | panic(err) 107 | } 108 | } 109 | return buf.Bytes() 110 | } 111 | 112 | func Example_stream() { 113 | plaintext := "example plaintext" 114 | 115 | encoded := exampleEncode([]byte(plaintext)) 116 | mps := newMsgpackStream(bytes.NewReader(encoded)) 117 | r := newChunkReader(newExampleChunker(mps)) 118 | 119 | decoded, err := io.ReadAll(r) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | fmt.Println(string(decoded)) 125 | // Output: example plaintext 126 | } 127 | 128 | type testChunker struct { 129 | t *testing.T 130 | chunks [][]byte 131 | finalErr error 132 | errWithLastChunk bool 133 | finalErrHit bool 134 | } 135 | 136 | func (c *testChunker) getNextChunk() ([]byte, error) { 137 | if c.finalErrHit { 138 | c.t.Fatal("getNextChunk() called with finalErrHit set") 139 | } 140 | 141 | if len(c.chunks) == 0 { 142 | // c.errWithLastChunk can still be set here if 143 | // chunkString is called with the empty string. 144 | c.finalErrHit = true 145 | return nil, c.finalErr 146 | } 147 | 148 | chunk := c.chunks[0] 149 | c.chunks = c.chunks[1:] 150 | if c.errWithLastChunk && len(c.chunks) == 0 { 151 | c.finalErrHit = true 152 | return chunk, c.finalErr 153 | } 154 | return chunk, nil 155 | } 156 | 157 | // chunkString chunks s up into pieces of size chunkSize, then returns 158 | // a testChunker to emit those chunks. 159 | func chunkString(t *testing.T, s string, chunkSize int, finalErr error, errWithLastChunk bool) *testChunker { 160 | var chunks [][]byte 161 | for len(s) > 0 { 162 | n := chunkSize 163 | if n > len(s) { 164 | n = len(s) 165 | } 166 | chunks = append(chunks, []byte(s[:n])) 167 | s = s[n:] 168 | } 169 | return &testChunker{t, chunks, finalErr, errWithLastChunk, false} 170 | } 171 | 172 | // readAll reads all data from r with the given buffer size. 173 | func readAll(t *testing.T, r io.Reader, bufSize int) ([]byte, error) { 174 | var out []byte 175 | buf := make([]byte, bufSize) 176 | for { 177 | n, err := r.Read(buf) 178 | if err == nil { 179 | assert.Equal(t, bufSize, n) 180 | } 181 | out = append(out, buf[:n]...) 182 | if err != nil { 183 | return out, err 184 | } 185 | } 186 | } 187 | 188 | func testChunkReader(t *testing.T, s string, chunkSize, bufSize int, finalErr error, errWithLastChunk bool) { 189 | chunker := chunkString(t, s, chunkSize, finalErr, errWithLastChunk) 190 | r := newChunkReader(chunker) 191 | out, err := readAll(t, r, bufSize) 192 | require.Equal(t, finalErr, err) 193 | require.Equal(t, s, string(out)) 194 | } 195 | 196 | func TestChunkReader(t *testing.T) { 197 | inputs := []string{ 198 | "", 199 | "hello world", 200 | "somewhat long string", 201 | string(make([]byte, 1024)), 202 | } 203 | 204 | sizes := []int{1, 3, 5, 1024} 205 | 206 | errs := []error{ 207 | errors.New("test error"), 208 | io.EOF, 209 | } 210 | 211 | for _, input := range inputs { 212 | for _, chunkSize := range sizes { 213 | for _, bufSize := range sizes { 214 | for _, finalErr := range errs { 215 | for _, errWithLastChunk := range []bool{false, true} { 216 | // Capture range variables. 217 | input := input 218 | chunkSize := chunkSize 219 | bufSize := bufSize 220 | finalErr := finalErr 221 | errWithLastChunk := errWithLastChunk 222 | 223 | var inputName string 224 | if len(input) > 5 { 225 | inputName = fmt.Sprintf("string(%d)", len(input)) 226 | } else { 227 | inputName = fmt.Sprintf("%q", input) 228 | } 229 | name := fmt.Sprintf("input=%s,chunkSize=%d,bufSize=%d,finalErr=%v,errWithLastChunk=%t", inputName, chunkSize, bufSize, finalErr, errWithLastChunk) 230 | t.Run(name, func(t *testing.T) { 231 | testChunkReader(t, input, chunkSize, bufSize, finalErr, errWithLastChunk) 232 | }) 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | func TestChunkReaderEmptyRead(t *testing.T) { 241 | s := "hello world" 242 | chunker := chunkString(t, s, 5, io.EOF, false) 243 | r := newChunkReader(chunker) 244 | 245 | n, err := r.Read(nil) 246 | require.NoError(t, err) 247 | require.Equal(t, 0, n) 248 | 249 | out, err := readAll(t, r, 1) 250 | require.Equal(t, io.EOF, err) 251 | require.Equal(t, s, string(out)) 252 | 253 | n, err = r.Read(nil) 254 | require.Equal(t, io.EOF, err) 255 | require.Equal(t, 0, n) 256 | } 257 | 258 | type badChunker struct{} 259 | 260 | func (c badChunker) getNextChunk() ([]byte, error) { 261 | return nil, nil 262 | } 263 | 264 | func TestChunkReaderBadChunker(t *testing.T) { 265 | r := newChunkReader(badChunker{}) 266 | 267 | require.Panics(t, func() { 268 | _, _ = r.Read(nil) 269 | }) 270 | } 271 | -------------------------------------------------------------------------------- /encoding/basex/encoding.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package basex 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "math" 11 | "math/big" 12 | ) 13 | 14 | // Encoding is a radix X encoding/decoding scheme, defined by X-length 15 | // character alphabet. 16 | type Encoding struct { 17 | encode []byte 18 | decodeMap [256](*big.Int) 19 | skipMap [256]bool 20 | base256BlockLen int 21 | baseXBlockLen int 22 | base int 23 | logOfBase float64 24 | baseBig *big.Int 25 | skipBytes string 26 | } 27 | 28 | // NewEncoding returns a new Encoding defined by the given alphabet, 29 | // which must a x-byte string. No padding options are currently allowed. 30 | // inBlock is the size of input blocks to consider. 31 | // 32 | // For base 58, we recommend 19-byte 33 | // input blocks, which encode to 26-byte output blocks with only .3 bits 34 | // wasted per block. The name of the game is to find a good rational 35 | // approximation of 8/log2(58), and 26/19 is pretty good! 36 | func NewEncoding(encoder string, base256BlockLen int, skipBytes string) *Encoding { 37 | base := len(encoder) 38 | 39 | logOfBase := math.Log2(float64(base)) 40 | 41 | // If input blocks are base256BlockLen size, compute the corresponding 42 | // output block length. We need to round up to fit the overflow. 43 | baseXBlockLen := int(math.Ceil(float64(8*base256BlockLen) / logOfBase)) 44 | 45 | // Code adapted from encoding/base64/base64.go in the standard 46 | // Go libraries. 47 | e := &Encoding{ 48 | encode: make([]byte, base), 49 | base: base, 50 | base256BlockLen: base256BlockLen, 51 | baseXBlockLen: baseXBlockLen, 52 | logOfBase: logOfBase, 53 | baseBig: big.NewInt(int64(base)), 54 | skipBytes: skipBytes, 55 | } 56 | copy(e.encode, encoder) 57 | 58 | for _, c := range skipBytes { 59 | e.skipMap[c] = true 60 | } 61 | for i := 0; i < len(encoder); i++ { 62 | e.decodeMap[encoder[i]] = big.NewInt(int64(i)) 63 | } 64 | return e 65 | } 66 | 67 | /* 68 | * Encoder 69 | */ 70 | 71 | // Encode encodes src using the encoding enc, writing 72 | // EncodedLen(len(src)) bytes to dst. 73 | // 74 | // The encoding aligns the input along base256BlockLen boundaries. 75 | // so Encode is not appropriate for use on individual blocks 76 | // of a large data stream. Use NewEncoder() instead. 77 | func (enc *Encoding) Encode(dst, src []byte) { 78 | for sp, dp, sLim, dLim := 0, 0, 0, 0; sp < len(src); sp, dp = sLim, dLim { 79 | sLim = sp + enc.base256BlockLen 80 | dLim = dp + enc.baseXBlockLen 81 | if sLim > len(src) { 82 | sLim = len(src) 83 | } 84 | if dLim > len(dst) { 85 | dLim = len(dst) 86 | } 87 | enc.encodeBlock(dst[dp:dLim], src[sp:sLim]) 88 | } 89 | } 90 | 91 | type byteType int 92 | 93 | const ( 94 | normalByteType byteType = 0 95 | skipByteType byteType = 1 96 | invalidByteType byteType = 2 97 | ) 98 | 99 | func (enc *Encoding) getByteType(b byte) byteType { 100 | if enc.decodeMap[b] != nil { 101 | return normalByteType 102 | } 103 | if enc.skipMap[b] { 104 | return skipByteType 105 | } 106 | return invalidByteType 107 | } 108 | 109 | func (enc *Encoding) hasSkipBytes() bool { 110 | return len(enc.skipBytes) > 0 111 | } 112 | 113 | // IsValidByte returns true if the given byte is valid in this 114 | // decoding. Can be either from the main alphabet or the skip 115 | // alphabet to be considered valid. 116 | func (enc *Encoding) IsValidByte(b byte) bool { 117 | return enc.decodeMap[b] != nil || enc.skipMap[b] 118 | } 119 | 120 | // encodeBlock fills the dst buffer with the encoding of src. 121 | // It is assumed the buffers are appropriately sized, and no 122 | // bounds checks are performed. In particular, the dst buffer will 123 | // be zero-padded from right to left in all remaining bytes. 124 | func (enc *Encoding) encodeBlock(dst, src []byte) { 125 | // Interpret the block as a big-endian number (Go's default) 126 | num := new(big.Int).SetBytes(src) 127 | rem := new(big.Int) 128 | quo := new(big.Int) 129 | 130 | encodedLen := enc.EncodedLen(len(src)) 131 | 132 | p := encodedLen - 1 133 | 134 | for num.Sign() != 0 { 135 | num, rem = quo.QuoRem(num, enc.baseBig, rem) 136 | dst[p] = enc.encode[rem.Uint64()] 137 | p-- 138 | } 139 | 140 | // Pad the remainder of the buffer with 0s 141 | for p >= 0 { 142 | dst[p] = enc.encode[0] 143 | p-- 144 | } 145 | } 146 | 147 | func (enc *Encoding) decode(dst []byte, src []byte) (n int, err error) { 148 | dp, sp := 0, 0 149 | for sp < len(src) { 150 | di, si, err := enc.decodeBlock(dst[dp:], src[sp:], sp) 151 | if err != nil { 152 | return dp, err 153 | } 154 | sp += si 155 | dp += di 156 | } 157 | return dp, nil 158 | } 159 | 160 | // Decode decodes src using the encoding enc. It writes at most 161 | // DecodedLen(len(src)) bytes to dst and returns the number of bytes 162 | // written. If src contains invalid baseX data, it will return the 163 | // number of bytes successfully written and CorruptInputError. It can 164 | // also return an ErrInvalidEncodingLength error if there is a non-standard 165 | // number of bytes in this encoding 166 | func (enc *Encoding) Decode(dst, src []byte) (n int, err error) { 167 | return enc.decode(dst, src) 168 | } 169 | 170 | // CorruptInputError is returned when Decode() finds a non-alphabet character 171 | type CorruptInputError int 172 | 173 | // Error fits the error interface 174 | func (e CorruptInputError) Error() string { 175 | return fmt.Sprintf("illegal data at input byte %d", int(e)) 176 | } 177 | 178 | // ErrInvalidEncodingLength is returned when a non-minimal encoding length is found 179 | var ErrInvalidEncodingLength = errors.New("invalid encoding length; either truncated or has trailing garbage") 180 | 181 | func (enc *Encoding) decodeBlock(dst []byte, src []byte, baseOffset int) (int, int, error) { 182 | si := 0 // source index 183 | numGoodChars := 0 184 | res := new(big.Int) 185 | res.SetUint64(0) 186 | 187 | for i, b := range src { 188 | v := enc.decodeMap[b] 189 | si++ 190 | 191 | if v == nil { 192 | if enc.skipMap[b] { 193 | continue 194 | } 195 | return 0, 0, CorruptInputError(i + baseOffset) 196 | } 197 | 198 | numGoodChars++ 199 | res.Mul(res, enc.baseBig) 200 | res.Add(res, v) 201 | 202 | if numGoodChars == enc.baseXBlockLen { 203 | break 204 | } 205 | } 206 | 207 | if !enc.IsValidEncodingLength(numGoodChars) { 208 | return 0, 0, ErrInvalidEncodingLength 209 | } 210 | 211 | paddedLen := enc.DecodedLen(numGoodChars) 212 | 213 | // Use big-endian representation (the default with Go's library) 214 | raw := res.Bytes() 215 | p := 0 216 | if len(raw) < paddedLen { 217 | p = paddedLen - len(raw) 218 | copy(dst, bytes.Repeat([]byte{0}, p)) 219 | } 220 | copy(dst[p:paddedLen], raw) 221 | return paddedLen, si, nil 222 | } 223 | 224 | // EncodedLen returns the length in bytes of the baseX encoding 225 | // of an input buffer of length n 226 | func (enc *Encoding) EncodedLen(n int) int { 227 | // Fast path! 228 | if n == enc.base256BlockLen { 229 | return enc.baseXBlockLen 230 | } 231 | 232 | nblocks := n / enc.base256BlockLen 233 | out := nblocks * enc.baseXBlockLen 234 | rem := n % enc.base256BlockLen 235 | if rem > 0 { 236 | out += int(math.Ceil(float64(rem*8) / enc.logOfBase)) 237 | } 238 | return out 239 | } 240 | 241 | // DecodedLen returns the length in bytes of the baseX decoding 242 | // of an input buffer of length n 243 | func (enc *Encoding) DecodedLen(n int) int { 244 | // Fast path! 245 | if n == enc.baseXBlockLen { 246 | return enc.base256BlockLen 247 | } 248 | 249 | nblocks := n / enc.baseXBlockLen 250 | out := nblocks * enc.base256BlockLen 251 | rem := n % enc.baseXBlockLen 252 | if rem > 0 { 253 | out += int(math.Floor(float64(rem) * enc.logOfBase / float64(8))) 254 | } 255 | return out 256 | } 257 | 258 | // IsValidEncodingLength returns true if this block has a valid encoding length. 259 | // An encoding length is invalid if a short encoding would have sufficed. 260 | func (enc *Encoding) IsValidEncodingLength(n int) bool { 261 | // Fast path! 262 | if n == enc.baseXBlockLen { 263 | return true 264 | } 265 | f := func(n int) int { 266 | return int(math.Floor(float64(n) * enc.logOfBase / float64(8))) 267 | } 268 | return f(n) != f(n-1) 269 | } 270 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Keybase, Inc. All rights reserved. Use of 2 | // this source code is governed by the included BSD license. 3 | 4 | package saltpack 5 | 6 | import ( 7 | "bytes" 8 | "crypto/hmac" 9 | "crypto/sha512" 10 | "encoding/binary" 11 | "fmt" 12 | 13 | "github.com/keybase/go-codec/codec" 14 | "golang.org/x/crypto/chacha20poly1305" 15 | ) 16 | 17 | // maxReceiverCount is the maximum number of receivers allowed 18 | // for a single encrypted saltpack message, which is the maximum length 19 | // of a msgpack array. 20 | const maxReceiverCount = (1 << 32) - 1 21 | 22 | // encryptionBlockNumber describes which block number we're at in the sequence 23 | // of encrypted blocks. Each encrypted block of course fits into a packet. 24 | type encryptionBlockNumber uint64 25 | 26 | func codecHandle() *codec.MsgpackHandle { 27 | var mh codec.MsgpackHandle 28 | mh.WriteExt = true 29 | return &mh 30 | } 31 | 32 | func (e encryptionBlockNumber) check() error { 33 | if e >= encryptionBlockNumber(0xffffffffffffffff) { 34 | return ErrPacketOverflow 35 | } 36 | return nil 37 | } 38 | 39 | // assertEndOfStream reads from stream, and converts a nil error into 40 | // ErrTrailingGarbage. Thus, it always returns a non-nil error. This 41 | // should be used in a context where io.EOF is expected, and anything 42 | // else is an error. 43 | func assertEndOfStream(stream *msgpackStream) error { 44 | var i interface{} 45 | _, err := stream.Read(&i) 46 | if err == nil { 47 | err = ErrTrailingGarbage 48 | } 49 | return err 50 | } 51 | 52 | type headerHash [sha512.Size]byte 53 | 54 | func attachedSignatureInput(version Version, headerHash headerHash, payloadChunk []byte, seqno packetSeqno, isFinal bool) []byte { 55 | hasher := sha512.New() 56 | _, _ = hasher.Write(headerHash[:]) 57 | _ = binary.Write(hasher, binary.BigEndian, seqno) 58 | switch version.Major { 59 | case 1: 60 | // Nothing to do. 61 | case 2: 62 | var isFinalByte byte 63 | if isFinal { 64 | isFinalByte = 1 65 | } 66 | _, _ = hasher.Write([]byte{isFinalByte}) 67 | default: 68 | panic(ErrBadVersion{version}) 69 | } 70 | _, _ = hasher.Write(payloadChunk) 71 | 72 | var buf bytes.Buffer 73 | _, _ = buf.Write([]byte(signatureAttachedString)) 74 | _, _ = buf.Write(hasher.Sum(nil)) 75 | 76 | return buf.Bytes() 77 | } 78 | 79 | func detachedSignatureInput(headerHash headerHash, plaintext []byte) []byte { 80 | hasher := sha512.New() 81 | _, _ = hasher.Write(headerHash[:]) 82 | _, _ = hasher.Write(plaintext) 83 | 84 | return detachedSignatureInputFromHash(hasher.Sum(nil)) 85 | } 86 | 87 | func detachedSignatureInputFromHash(plaintextAndHeaderHash []byte) []byte { 88 | var buf bytes.Buffer 89 | _, _ = buf.Write([]byte(signatureDetachedString)) 90 | _, _ = buf.Write(plaintextAndHeaderHash) 91 | 92 | return buf.Bytes() 93 | } 94 | 95 | func copyEqualSize(out, in []byte) { 96 | if len(out) != len(in) { 97 | panic(fmt.Sprintf("len(out)=%d != len(in)=%d", len(out), len(in))) 98 | } 99 | copy(out, in) 100 | } 101 | 102 | func copyEqualSizeStr(out []byte, in string) { 103 | if len(out) != len(in) { 104 | panic(fmt.Sprintf("len(out)=%d != len(in)=%d", len(out), len(in))) 105 | } 106 | copy(out, in) 107 | } 108 | 109 | func sliceToByte24(in []byte) [24]byte { 110 | var out [24]byte 111 | copyEqualSize(out[:], in) 112 | return out 113 | } 114 | 115 | func stringToByte24(in string) [24]byte { 116 | var out [24]byte 117 | copyEqualSizeStr(out[:], in) 118 | return out 119 | } 120 | 121 | func sliceToByte32(in []byte) [32]byte { 122 | var out [32]byte 123 | copyEqualSize(out[:], in) 124 | return out 125 | } 126 | 127 | func sliceToByte64(in []byte) [64]byte { 128 | var out [64]byte 129 | copyEqualSize(out[:], in) 130 | return out 131 | } 132 | 133 | type macKey [cryptoAuthKeyBytes]byte 134 | 135 | type payloadHash [sha512.Size]byte 136 | 137 | type payloadAuthenticator [cryptoAuthBytes]byte 138 | 139 | func (pa payloadAuthenticator) Equal(other payloadAuthenticator) bool { 140 | return hmac.Equal(pa[:], other[:]) 141 | } 142 | 143 | func computePayloadAuthenticator(macKey macKey, payloadHash payloadHash) payloadAuthenticator { 144 | // Equivalent to crypto_auth, but using Go's builtin HMAC. Truncates 145 | // SHA512, instead of calling SHA512/256, which has different IVs. 146 | authenticatorDigest := hmac.New(sha512.New, macKey[:]) 147 | _, _ = authenticatorDigest.Write(payloadHash[:]) 148 | fullMAC := authenticatorDigest.Sum(nil) 149 | return sliceToByte32(fullMAC[:cryptoAuthBytes]) 150 | } 151 | 152 | func computeMACKeySingle(secret BoxSecretKey, public BoxPublicKey, nonce Nonce) macKey { 153 | macKeyBox := secret.Box(public, nonce, make([]byte, cryptoAuthKeyBytes)) 154 | return sliceToByte32(macKeyBox[chacha20poly1305.Overhead : chacha20poly1305.Overhead+cryptoAuthKeyBytes]) 155 | } 156 | 157 | func sum512Truncate256(in []byte) [32]byte { 158 | // Consistent with computePayloadAuthenticator in that it 159 | // truncates SHA512 instead of calling SHA512/256, which has 160 | // different IVs. 161 | sum512 := sha512.Sum512(in) 162 | return sliceToByte32(sum512[:32]) 163 | } 164 | 165 | func computePayloadHash(version Version, headerHash headerHash, nonce Nonce, ciphertext []byte, isFinal bool) payloadHash { 166 | payloadDigest := sha512.New() 167 | _, _ = payloadDigest.Write(headerHash[:]) 168 | _, _ = payloadDigest.Write(nonce[:]) 169 | switch version.Major { 170 | case 1: 171 | // Nothing to do. 172 | case 2: 173 | var isFinalByte byte 174 | if isFinal { 175 | isFinalByte = 1 176 | } 177 | _, _ = payloadDigest.Write([]byte{isFinalByte}) 178 | default: 179 | panic(ErrBadVersion{version}) 180 | } 181 | _, _ = payloadDigest.Write(ciphertext) 182 | h := payloadDigest.Sum(nil) 183 | return sliceToByte64(h) 184 | } 185 | 186 | func computeSigncryptionSignatureInput(headerHash headerHash, nonce Nonce, isFinal bool, chunkPlaintext []byte) []byte { 187 | signatureInput := []byte(signatureEncryptedString) 188 | // This is a bit redundant, as the nonce already contains part 189 | // of the header hash and the isFinal flag. However, we 190 | // truncate the header hash pretty severely for the nonce, so 191 | // it seems a bit safer to be redundant. 192 | signatureInput = append(signatureInput, headerHash[:]...) 193 | signatureInput = append(signatureInput, nonce[:]...) 194 | var isFinalByte byte 195 | if isFinal { 196 | isFinalByte = 1 197 | } 198 | signatureInput = append(signatureInput, isFinalByte) 199 | plaintextHash := sha512.Sum512(chunkPlaintext) 200 | signatureInput = append(signatureInput, plaintextHash[:]...) 201 | return signatureInput 202 | } 203 | 204 | func hashHeader(headerBytes []byte) headerHash { 205 | return sha512.Sum512(headerBytes) 206 | } 207 | 208 | // VersionValidator is a function that takes a version and returns nil 209 | // if it's a valid version, and an error otherwise. 210 | type VersionValidator func(version Version) error 211 | 212 | // CheckKnownMajorVersion returns nil if the given version has a known 213 | // major version. You probably want to use this with NewDecryptStream, 214 | // unless you want to restrict to specific versions only. 215 | func CheckKnownMajorVersion(version Version) error { 216 | for _, knownVersion := range KnownVersions() { 217 | if version.Major == knownVersion.Major { 218 | return nil 219 | } 220 | } 221 | return ErrBadVersion{version} 222 | } 223 | 224 | // SingleVersionValidator returns a VersionValidator that returns nil 225 | // if its given version is equal to desiredVersion. 226 | func SingleVersionValidator(desiredVersion Version) VersionValidator { 227 | return func(version Version) error { 228 | if version == desiredVersion { 229 | return nil 230 | } 231 | 232 | return ErrBadVersion{version} 233 | } 234 | } 235 | 236 | func checkChunkState(version Version, chunkLen int, blockIndex uint64, isFinal bool) error { 237 | switch version.Major { 238 | case 1: 239 | // For V1, we derive isFinal from the chunk length, so 240 | // if there's a mismatch, that's a bug and not a 241 | // stream error. 242 | if (chunkLen == 0) != isFinal { 243 | panic(fmt.Sprintf("chunkLen=%d and isFinal=%t", chunkLen, isFinal)) 244 | } 245 | 246 | case 2: 247 | // TODO: Ideally, we'd have tests exercising this case. 248 | if (chunkLen == 0) && (blockIndex != 0 || !isFinal) { 249 | return ErrUnexpectedEmptyBlock 250 | } 251 | 252 | default: 253 | panic(ErrBadVersion{version}) 254 | } 255 | 256 | return nil 257 | } 258 | 259 | // assertEncodedChunkState sanity-checks some encoded chunk parameters. 260 | func assertEncodedChunkState(version Version, encodedChunk []byte, encodingOverhead int, blockIndex uint64, isFinal bool) { 261 | if len(encodedChunk) < encodingOverhead { 262 | panic("encodedChunk is too small") 263 | } 264 | 265 | err := checkChunkState(version, len(encodedChunk)-encodingOverhead, blockIndex, isFinal) 266 | if err != nil { 267 | panic(err) 268 | } 269 | } 270 | 271 | // checkDecodedChunkState sanity-checks some decoded chunk 272 | // parameters. A returned error means there's something wrong with the 273 | // decoded stream. 274 | func checkDecodedChunkState(version Version, chunk []byte, seqno packetSeqno, isFinal bool) error { 275 | // The first decoded block has seqno 1, since the header bytes 276 | // are decoded first. 277 | return checkChunkState(version, len(chunk), uint64(seqno-1), isFinal) 278 | } 279 | --------------------------------------------------------------------------------