├── .github └── workflows │ └── test.yml ├── .gitmodules ├── AUTHORS ├── LICENSE ├── LICENSES ├── MIT.txt └── Unlicense.txt ├── README.md ├── boxstream ├── .gitignore ├── box.go ├── box_test.go ├── box_test.js ├── interop_test.go ├── package-lock.json ├── package-lock.json.license ├── package.json ├── package.json.license ├── unbox.go └── unbox_test.js ├── client.go ├── conn.go ├── go.mod ├── go.sum ├── go.sum.license ├── internal └── lo25519 │ └── ed25519.go ├── net_test.go ├── secrethandshake ├── .gitignore ├── client_test.go ├── client_test.js ├── conn.go ├── conn_test.go ├── errors.go ├── errors_test.go ├── genkey.js ├── helpers_test.go ├── internal │ └── extra25519 │ │ ├── convert.go │ │ └── convert_test.go ├── key.alice.json ├── key.alice.json.license ├── key.bob.json ├── key.bob.json.license ├── package-lock.json ├── package-lock.json.license ├── package.json ├── package.json.license ├── server_test.go ├── server_test.js ├── state.go ├── string.go └── tests │ ├── Makefile │ ├── client.go │ └── server.go └── server.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | name: Test 6 | 7 | on: 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | 15 | build: 16 | name: Build 17 | runs-on: ubuntu-latest 18 | steps: 19 | 20 | - name: Set up Node for interop testing 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: 14.x 24 | 25 | - name: Set up Go 1.x 26 | uses: actions/setup-go@v2 27 | with: 28 | go-version: ^1.16 29 | id: go 30 | 31 | - name: Check out code into the Go module directory 32 | uses: actions/checkout@v2 33 | 34 | - name: Get dependencies 35 | run: go get -v -t -d ./... 36 | 37 | - name: Build smoke test 38 | run: go build -v 39 | 40 | - name: Build dev smoke test 41 | run: go build -v -tags dev 42 | 43 | - name: install node ssb-stack 44 | run: | 45 | pushd boxstream 46 | npm ci 47 | popd 48 | pushd secrethandshake 49 | npm ci 50 | popd 51 | 52 | - name: Test 53 | run: go test ./... 54 | 55 | - name: Test against shs1-testsuite 56 | run: cd secrethandshake/tests && make test 57 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | [submodule "secrethandshake/shs1-testsuite"] 6 | path = secrethandshake/shs1-testsuite 7 | #url = http://localhost:7718/%25riikqU1Zc%2Fdgjc80vABMA3DkTzTHzlxEYxGU5NYwje8%3D.sha256 8 | url = https://github.com/AljoschaMeyer/shs1-testsuite.git 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: Unlicense 4 | 5 | cel 6 | Henry Bubert 7 | Jan Winkelmann 8 | lukechampine 9 | Maximilian Güntner 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Henry Bubert, Jan Winkelmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSES/Unlicense.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. 4 | 5 | In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and 6 | successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | For more information, please refer to 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # secretstream [![Build Status](https://travis-ci.org/cryptoscope/secretstream.svg?branch=master)](https://travis-ci.org/cryptoscope/secretstream) [![GoDoc](https://godoc.org/go.cryptoscope.co/secretstream?status.svg)](https://godoc.org/go.cryptoscope.co/secretstream) [![Go Report Card](https://goreportcard.com/badge/go.cryptoscope.co/secretstream)](https://goreportcard.com/report/go.cryptoscope.co/secretstream) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![REUSE status](https://api.reuse.software/badge/github.com/cryptoscope/secretstream)](https://api.reuse.software/info/github.com/cryptoscope/secretstream) 8 | 9 | A port of [secret-handshake](https://github.com/auditdrivencrypto/secret-handshake) to [Go](https://golang.org). 10 | 11 | Provides an encrypted bidirectional stream using two [boxstream]s. 12 | Uses [secret-handshake] to negotiate the keys and nonces. 13 | 14 | [boxstream]: https://github.com/dominictarr/pull-box-stream 15 | [secret-handshake]: https://github.com/auditdrivencrypto/secret-handshake 16 | 17 | 18 | ## Development 19 | 20 | If you want to run the compatability tests against the nodejs implementation, run `npm ci && go test -tags interop_nodejs` on the `secrethandshake` and `boxstream` sub-packages. 21 | 22 | -------------------------------------------------------------------------------- /boxstream/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | node_modules 6 | -------------------------------------------------------------------------------- /boxstream/box.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package boxstream implements npm:pull-box-stream in Go (without the pull) 6 | // 7 | // https://github.com/dominictarr/pull-box-stream 8 | package boxstream 9 | 10 | import ( 11 | "encoding/binary" 12 | "io" 13 | "sync" 14 | 15 | "golang.org/x/crypto/nacl/secretbox" 16 | ) 17 | 18 | const ( 19 | // HeaderLength defines the length of the header packet before the body 20 | HeaderLength = 2 + 16 + 16 21 | 22 | // MaxSegmentSize is the maximum body size for boxstream packets 23 | MaxSegmentSize = 4 * 1024 24 | ) 25 | 26 | var goodbye [18]byte 27 | 28 | // Boxer encrypts everything that is written to it 29 | type Boxer struct { 30 | l sync.Mutex 31 | w io.Writer 32 | secret *[32]byte 33 | nonce *[24]byte 34 | } 35 | 36 | // WriteMessage writes a boxstream packet to the underlying writer. len(msg) 37 | // must not exceed MaxSegmentSize. 38 | func (b *Boxer) WriteMessage(msg []byte) error { 39 | if len(msg) > MaxSegmentSize { 40 | panic("message exceeds maximum segment size") 41 | } 42 | b.l.Lock() 43 | defer b.l.Unlock() 44 | 45 | headerNonce := *b.nonce 46 | increment(b.nonce) 47 | bodyNonce := *b.nonce 48 | increment(b.nonce) 49 | 50 | // construct body box 51 | bodyBox := secretbox.Seal(nil, msg, &bodyNonce, b.secret) 52 | bodyMAC, body := bodyBox[:secretbox.Overhead], bodyBox[secretbox.Overhead:] 53 | 54 | // construct header box 55 | header := make([]byte, 2+secretbox.Overhead) 56 | binary.BigEndian.PutUint16(header[:2], uint16(len(msg))) 57 | copy(header[2:], bodyMAC) 58 | headerBox := secretbox.Seal(nil, header, &headerNonce, b.secret) 59 | 60 | // write header + body 61 | if _, err := b.w.Write(headerBox); err != nil { 62 | return err 63 | } 64 | _, err := b.w.Write(body) 65 | return err 66 | } 67 | 68 | // WriteGoodbye writes the 'goodbye' protocol message to the underlying writer. 69 | func (b *Boxer) WriteGoodbye() error { 70 | b.l.Lock() 71 | defer b.l.Unlock() 72 | _, err := b.w.Write(secretbox.Seal(nil, goodbye[:], b.nonce, b.secret)) 73 | return err 74 | } 75 | 76 | // NewBoxer returns a Boxer that writes encrypted messages to w. 77 | func NewBoxer(w io.Writer, nonce *[24]byte, secret *[32]byte) *Boxer { 78 | return &Boxer{ 79 | w: w, 80 | secret: secret, 81 | nonce: nonce, 82 | } 83 | } 84 | 85 | func increment(b *[24]byte) *[24]byte { 86 | var i int 87 | for i = len(b) - 1; i >= 0 && b[i] == 0xff; i-- { 88 | b[i] = 0 89 | } 90 | 91 | if i < 0 { 92 | return b 93 | } 94 | 95 | b[i] = b[i] + 1 96 | 97 | return b 98 | } 99 | -------------------------------------------------------------------------------- /boxstream/box_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package boxstream 6 | 7 | import ( 8 | "io" 9 | "net" 10 | "testing" 11 | ) 12 | 13 | func mkCheckOnce(errc chan<- error) func(error) { 14 | return func(err error) { 15 | if err != nil { 16 | errc <- err 17 | } else { 18 | close(errc) 19 | } 20 | } 21 | } 22 | 23 | func TestBox(t *testing.T) { 24 | pr, pw := net.Pipe() 25 | 26 | var secret [32]byte 27 | var boxnonce [24]byte 28 | var unboxnonce [24]byte 29 | 30 | for i := range secret { 31 | secret[i] = byte(3 * i) 32 | } 33 | for i := range boxnonce { 34 | boxnonce[i] = byte(5 * i) 35 | } 36 | 37 | copy(unboxnonce[:], boxnonce[:]) 38 | 39 | bw := NewBoxer(pw, &boxnonce, &secret) 40 | br := NewUnboxer(pr, &unboxnonce, &secret) 41 | 42 | wErrc := make(chan error) 43 | checkW := mkCheckOnce(wErrc) 44 | cErrc := make(chan error) 45 | checkC := mkCheckOnce(cErrc) 46 | go func() { 47 | err := bw.WriteMessage([]byte{0, 1, 2, 3, 4, 5}) 48 | checkW(err) 49 | 50 | err = bw.WriteGoodbye() 51 | checkC(err) 52 | }() 53 | 54 | rx, err := br.ReadMessage() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | if e, ok := <-wErrc; ok { 59 | t.Fatal(e) 60 | } 61 | if len(rx) != 6 { 62 | t.Error("rx len wrong") 63 | } 64 | 65 | for i, x := range rx { 66 | if i != int(x) { 67 | t.Errorf("expected %v, got %v", i, x) 68 | } 69 | } 70 | 71 | rx, err = br.ReadMessage() 72 | if err != io.EOF { 73 | t.Fatal(err) 74 | } else if len(rx) != 0 { 75 | t.Errorf("more data?") 76 | } 77 | 78 | if e, ok := <-cErrc; ok { 79 | t.Fatal(e) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /boxstream/box_test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | var boxes = require('pull-box-stream') 6 | var pull = require('pull-stream') 7 | var toPull = require('stream-to-pull-stream') 8 | 9 | pull( 10 | toPull.source(process.stdin), 11 | boxes.createBoxStream(Buffer.from(process.argv[2], 'base64'), Buffer.from(process.argv[3], 'base64')), 12 | toPull.sink(process.stdout) 13 | ) 14 | -------------------------------------------------------------------------------- /boxstream/interop_test.go: -------------------------------------------------------------------------------- 1 | // +build interop_nodejs 2 | 3 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 4 | // 5 | // SPDX-License-Identifier: MIT 6 | 7 | package boxstream 8 | 9 | import ( 10 | "bufio" 11 | "crypto/rand" 12 | "encoding/base64" 13 | "fmt" 14 | "io" 15 | "strings" 16 | "testing" 17 | 18 | "go.mindeco.de/logging/logtest" 19 | "go.mindeco.de/proc" 20 | ) 21 | 22 | const cnt = 1000 23 | 24 | func check(err error) { 25 | if err != nil { 26 | panic(err) 27 | } 28 | } 29 | 30 | func TestInterop_WriteToJS(t *testing.T) { 31 | var key [32]byte 32 | var nonce [24]byte 33 | 34 | _, err := io.ReadFull(rand.Reader, key[:]) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | _, err = io.ReadFull(rand.Reader, nonce[:]) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | node, err := proc.StartStdioProcess("node", logtest.Logger("unbox.js", t), "unbox_test.js", 45 | base64.StdEncoding.EncodeToString(key[:]), 46 | base64.StdEncoding.EncodeToString(nonce[:]), 47 | ) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | w := NewBoxer(node, &nonce, &key) 53 | want := strings.Repeat("Hello, Tests!", cnt) 54 | if _, err := fmt.Fprintln(w, want); err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | s := bufio.NewScanner(node) 59 | for s.Scan() { 60 | got := s.Text() 61 | t.Log(got) 62 | if got == want { 63 | break 64 | } 65 | t.Errorf("test data missmatch! got:%q", got) 66 | } 67 | } 68 | 69 | func TestInterop_ReadFromJS(t *testing.T) { 70 | var key [32]byte 71 | var nonce [24]byte 72 | 73 | _, err := io.ReadFull(rand.Reader, key[:]) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | node, err := proc.StartStdioProcess("node", logtest.Logger("box.js", t), "box_test.js", 83 | base64.StdEncoding.EncodeToString(key[:]), 84 | base64.StdEncoding.EncodeToString(nonce[:]), 85 | ) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | r := NewUnboxer(node, &nonce, &key) 91 | 92 | want := strings.Repeat("Hello, Tests!", cnt) 93 | if _, err := fmt.Fprintln(node, want); err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | s := bufio.NewScanner(r) 98 | for s.Scan() { 99 | got := s.Text() 100 | t.Log(got) 101 | if got == want { 102 | break 103 | } 104 | t.Errorf("test data missmatch! got:%q", got) 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /boxstream/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "chloride": { 6 | "version": "2.4.1", 7 | "resolved": "https://registry.npmjs.org/chloride/-/chloride-2.4.1.tgz", 8 | "integrity": "sha512-ZiID87W2o2llvuF4C7Fvt9GJisazSdMsSkjAq4WaMed9zn77nlkcy08ZfrPtOGAXyaxTDj0VjnuyD97EdJLz3g==", 9 | "dev": true, 10 | "requires": { 11 | "sodium-browserify": "^1.2.7", 12 | "sodium-browserify-tweetnacl": "^0.2.5", 13 | "sodium-chloride": "^1.1.2", 14 | "sodium-native": "^3.0.0" 15 | } 16 | }, 17 | "chloride-test": { 18 | "version": "1.2.4", 19 | "resolved": "https://registry.npmjs.org/chloride-test/-/chloride-test-1.2.4.tgz", 20 | "integrity": "sha512-9vhoi1qXSBPn6//ZxIgSe3M2QhKHzIPZQzmrZgmPADsqW0Jxpe3db1e7aGSRUMXbxAQ04SfypdT8dGaSvIvKDw==", 21 | "dev": true, 22 | "requires": { 23 | "json-buffer": "^2.0.11" 24 | } 25 | }, 26 | "ed2curve": { 27 | "version": "0.1.4", 28 | "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.1.4.tgz", 29 | "integrity": "sha1-lKRCSLuH2jXbDv968KpXYWgRf1k=", 30 | "dev": true, 31 | "requires": { 32 | "tweetnacl": "0.x.x" 33 | } 34 | }, 35 | "increment-buffer": { 36 | "version": "1.0.1", 37 | "resolved": "https://registry.npmjs.org/increment-buffer/-/increment-buffer-1.0.1.tgz", 38 | "integrity": "sha1-ZQdtdRidgIs5rROrW5WOBSFvng0=", 39 | "dev": true 40 | }, 41 | "inherits": { 42 | "version": "2.0.4", 43 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 44 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 45 | "dev": true 46 | }, 47 | "ini": { 48 | "version": "1.3.8", 49 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 50 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 51 | "dev": true, 52 | "optional": true 53 | }, 54 | "json-buffer": { 55 | "version": "2.0.11", 56 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-2.0.11.tgz", 57 | "integrity": "sha1-PkQf2jCYvo0eMXGtWRvGKjPi1V8=", 58 | "dev": true 59 | }, 60 | "libsodium": { 61 | "version": "0.7.9", 62 | "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz", 63 | "integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==", 64 | "dev": true 65 | }, 66 | "libsodium-wrappers": { 67 | "version": "0.7.9", 68 | "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", 69 | "integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==", 70 | "dev": true, 71 | "requires": { 72 | "libsodium": "^0.7.0" 73 | } 74 | }, 75 | "looper": { 76 | "version": "3.0.0", 77 | "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", 78 | "integrity": "sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k=", 79 | "dev": true 80 | }, 81 | "node-gyp-build": { 82 | "version": "4.2.3", 83 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", 84 | "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", 85 | "dev": true, 86 | "optional": true 87 | }, 88 | "pull-box-stream": { 89 | "version": "1.0.13", 90 | "resolved": "https://registry.npmjs.org/pull-box-stream/-/pull-box-stream-1.0.13.tgz", 91 | "integrity": "sha1-w+JAOY6rP1lRsu0QeMWYi/egork=", 92 | "dev": true, 93 | "requires": { 94 | "chloride": "^2.2.7", 95 | "increment-buffer": "~1.0.0", 96 | "pull-reader": "^1.2.5", 97 | "pull-stream": "^3.2.3", 98 | "pull-through": "^1.0.18", 99 | "split-buffer": "~1.0.0" 100 | } 101 | }, 102 | "pull-reader": { 103 | "version": "1.3.1", 104 | "resolved": "https://registry.npmjs.org/pull-reader/-/pull-reader-1.3.1.tgz", 105 | "integrity": "sha512-CBkejkE5nX50SiSEzu0Qoz4POTJMS/mw8G6aj3h3M/RJoKgggLxyF0IyTZ0mmpXFlXRcLmLmIEW4xeYn7AeDYw==", 106 | "dev": true 107 | }, 108 | "pull-stream": { 109 | "version": "3.6.14", 110 | "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.14.tgz", 111 | "integrity": "sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew==", 112 | "dev": true 113 | }, 114 | "pull-through": { 115 | "version": "1.0.18", 116 | "resolved": "https://registry.npmjs.org/pull-through/-/pull-through-1.0.18.tgz", 117 | "integrity": "sha1-jdYjFCY+Wc9Qlur7sSeitu8xBzU=", 118 | "dev": true, 119 | "requires": { 120 | "looper": "~3.0.0" 121 | } 122 | }, 123 | "safe-buffer": { 124 | "version": "5.2.1", 125 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 126 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 127 | "dev": true 128 | }, 129 | "sha.js": { 130 | "version": "2.4.5", 131 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz", 132 | "integrity": "sha1-J9Fx78yCoRi5ljn/WBZgJCtQbnw=", 133 | "dev": true, 134 | "requires": { 135 | "inherits": "^2.0.1" 136 | } 137 | }, 138 | "sodium-browserify": { 139 | "version": "1.3.0", 140 | "resolved": "https://registry.npmjs.org/sodium-browserify/-/sodium-browserify-1.3.0.tgz", 141 | "integrity": "sha512-1KRS6Oew3X13AIZhbmGF0YBdt2pQdafJMfv83OZHWbzxG92YBBnN8HYx/VKmYB4xCe90eidNaDJWBEFw/o3ahw==", 142 | "dev": true, 143 | "requires": { 144 | "libsodium-wrappers": "^0.7.4", 145 | "sha.js": "2.4.5", 146 | "sodium-browserify-tweetnacl": "^0.2.5", 147 | "tweetnacl": "^0.14.1" 148 | } 149 | }, 150 | "sodium-browserify-tweetnacl": { 151 | "version": "0.2.6", 152 | "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.6.tgz", 153 | "integrity": "sha512-ZnEI26hdluilpYY28Xc4rc1ALfmEp2TWihkJX6Mdtw0z9RfHfpZJU7P8DoKbN1HcBdU9aJmguFZs7igE8nLJPg==", 154 | "dev": true, 155 | "requires": { 156 | "chloride-test": "^1.1.0", 157 | "ed2curve": "^0.1.4", 158 | "sha.js": "^2.4.8", 159 | "tweetnacl": "^1.0.1", 160 | "tweetnacl-auth": "^0.3.0" 161 | }, 162 | "dependencies": { 163 | "sha.js": { 164 | "version": "2.4.11", 165 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 166 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 167 | "dev": true, 168 | "requires": { 169 | "inherits": "^2.0.1", 170 | "safe-buffer": "^5.0.1" 171 | } 172 | }, 173 | "tweetnacl": { 174 | "version": "1.0.3", 175 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", 176 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", 177 | "dev": true 178 | } 179 | } 180 | }, 181 | "sodium-chloride": { 182 | "version": "1.1.2", 183 | "resolved": "https://registry.npmjs.org/sodium-chloride/-/sodium-chloride-1.1.2.tgz", 184 | "integrity": "sha512-8AVzr9VHueXqfzfkzUA0aXe/Q4XG3UTmhlP6Pt+HQc5bbAPIJFo7ZIMh9tvn+99QuiMcyDJdYumegGAczl0N+g==", 185 | "dev": true 186 | }, 187 | "sodium-native": { 188 | "version": "3.2.1", 189 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.2.1.tgz", 190 | "integrity": "sha512-EgDZ/Z7PxL2kCasKk7wnRkV8W9kvwuIlHuHXAxkQm3FF0MgVsjyLBXGjSRGhjE6u7rhSpk3KaMfFM23bfMysIQ==", 191 | "dev": true, 192 | "optional": true, 193 | "requires": { 194 | "ini": "^1.3.5", 195 | "node-gyp-build": "^4.2.0" 196 | } 197 | }, 198 | "split-buffer": { 199 | "version": "1.0.0", 200 | "resolved": "https://registry.npmjs.org/split-buffer/-/split-buffer-1.0.0.tgz", 201 | "integrity": "sha1-t+jgq1E0UVi3LB9tvvJAbVHx0Cc=", 202 | "dev": true 203 | }, 204 | "stream-to-pull-stream": { 205 | "version": "1.7.3", 206 | "resolved": "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz", 207 | "integrity": "sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==", 208 | "dev": true, 209 | "requires": { 210 | "looper": "^3.0.0", 211 | "pull-stream": "^3.2.3" 212 | } 213 | }, 214 | "tweetnacl": { 215 | "version": "0.14.5", 216 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 217 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 218 | "dev": true 219 | }, 220 | "tweetnacl-auth": { 221 | "version": "0.3.1", 222 | "resolved": "https://registry.npmjs.org/tweetnacl-auth/-/tweetnacl-auth-0.3.1.tgz", 223 | "integrity": "sha1-t1vC3xVkm7hOi5qjwGacbEvODSU=", 224 | "dev": true, 225 | "requires": { 226 | "tweetnacl": "0.x.x" 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /boxstream/package-lock.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /boxstream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": {}, 3 | "devDependencies": { 4 | "chloride": "^2.4.1", 5 | "pull-box-stream": "^1.0.13", 6 | "pull-stream": "^3.6.14", 7 | "stream-to-pull-stream": "^1.7.3" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /boxstream/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /boxstream/unbox.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package boxstream 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "errors" 11 | "io" 12 | 13 | "golang.org/x/crypto/nacl/secretbox" 14 | ) 15 | 16 | // Unboxer decrypts everything that is read from it 17 | type Unboxer struct { 18 | r io.Reader 19 | buf [MaxSegmentSize + secretbox.Overhead]byte 20 | secret *[32]byte 21 | nonce *[24]byte 22 | } 23 | 24 | // ReadMessage reads the next message from the underlying stream. If the next 25 | // message was a 'goodbye', it returns io.EOF. 26 | func (u *Unboxer) ReadMessage() ([]byte, error) { 27 | headerNonce := *u.nonce 28 | increment(u.nonce) 29 | bodyNonce := *u.nonce 30 | increment(u.nonce) 31 | 32 | // read and unbox header 33 | headerBox := u.buf[:HeaderLength] 34 | if _, err := io.ReadFull(u.r, headerBox); err != nil { 35 | return nil, err 36 | } 37 | headerBuf := make([]byte, 0, 18) 38 | header, ok := secretbox.Open(headerBuf, headerBox, &headerNonce, u.secret) 39 | if !ok { 40 | return nil, errors.New("invalid header box") 41 | } 42 | 43 | // zero header indicates termination 44 | if bytes.Equal(header, goodbye[:]) { 45 | return nil, io.EOF 46 | } 47 | 48 | // read and unbox body 49 | bodyLen := binary.BigEndian.Uint16(header[:2]) 50 | if bodyLen > MaxSegmentSize { 51 | return nil, errors.New("message exceeds maximum segment size") 52 | } 53 | bodyBox := u.buf[:bodyLen+secretbox.Overhead] 54 | if _, err := io.ReadFull(u.r, bodyBox[secretbox.Overhead:]); err != nil { 55 | return nil, err 56 | } 57 | // prepend with MAC from header 58 | copy(bodyBox, header[2:]) 59 | msg, ok := secretbox.Open(nil, bodyBox, &bodyNonce, u.secret) 60 | if !ok { 61 | return nil, errors.New("invalid body box") 62 | } 63 | return msg, nil 64 | } 65 | 66 | // NewUnboxer wraps the passed Reader into an Unboxer. 67 | func NewUnboxer(r io.Reader, nonce *[24]byte, secret *[32]byte) *Unboxer { 68 | return &Unboxer{ 69 | r: r, 70 | secret: secret, 71 | nonce: nonce, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /boxstream/unbox_test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | var boxes = require('pull-box-stream') 6 | var pull = require('pull-stream') 7 | var toPull = require('stream-to-pull-stream') 8 | 9 | pull( 10 | toPull.source(process.stdin), 11 | boxes.createUnboxStream(Buffer.from(process.argv[2], 'base64'), Buffer.from(process.argv[3], 'base64')), 12 | toPull.sink(process.stdout) 13 | ) 14 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secretstream // import "github.com/ssbc/go-secretstream" 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "time" 11 | 12 | "github.com/ssbc/go-secretstream/boxstream" 13 | "github.com/ssbc/go-secretstream/secrethandshake" 14 | 15 | "github.com/ssbc/go-netwrap" 16 | ) 17 | 18 | // Client can dial secret-handshake server endpoints 19 | type Client struct { 20 | appKey []byte 21 | kp secrethandshake.EdKeyPair 22 | } 23 | 24 | // NewClient creates a new Client with the passed keyPair and appKey 25 | func NewClient(kp secrethandshake.EdKeyPair, appKey []byte) (*Client, error) { 26 | // TODO: consistancy check?!.. 27 | return &Client{ 28 | appKey: appKey, 29 | kp: kp, 30 | }, nil 31 | } 32 | 33 | // ConnWrapper returns a connection wrapper for the client. 34 | func (c *Client) ConnWrapper(pubKey []byte) netwrap.ConnWrapper { 35 | return func(conn net.Conn) (net.Conn, error) { 36 | state, err := secrethandshake.NewClientState(c.appKey, c.kp, pubKey) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | errc := make(chan error) 42 | go func() { 43 | errc <- secrethandshake.Client(state, conn) 44 | close(errc) 45 | }() 46 | 47 | select { 48 | case err := <-errc: 49 | if err != nil { 50 | return nil, err 51 | } 52 | case <-time.After(30 * time.Second): 53 | return nil, fmt.Errorf("secretstream: handshake timeout") 54 | } 55 | 56 | enKey, enNonce := state.GetBoxstreamEncKeys() 57 | deKey, deNonce := state.GetBoxstreamDecKeys() 58 | 59 | boxed := &Conn{ 60 | boxer: boxstream.NewBoxer(conn, &enNonce, &enKey), 61 | unboxer: boxstream.NewUnboxer(conn, &deNonce, &deKey), 62 | conn: conn, 63 | local: c.kp.Public[:], 64 | remote: state.Remote(), 65 | } 66 | 67 | return boxed, nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secretstream 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "errors" 11 | "net" 12 | "os" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/ssbc/go-secretstream/boxstream" 17 | 18 | "github.com/ssbc/go-netwrap" 19 | ) 20 | 21 | const NetworkString = "shs-bs" 22 | 23 | // Addr wrapps a net.Addr and adds the public key 24 | type Addr struct { 25 | PubKey []byte 26 | } 27 | 28 | // Network returns NetworkString, the network id of this protocol. 29 | // Can be used with github.com/go-netwrap to wrap the underlying connection. 30 | func (a Addr) Network() string { 31 | return NetworkString 32 | } 33 | 34 | func (a Addr) String() string { 35 | // TODO keks: is this the address format we want to use? 36 | return "@" + base64.StdEncoding.EncodeToString(a.PubKey) + ".ed25519" 37 | } 38 | 39 | // Conn is a boxstream wrapped net.Conn 40 | type Conn struct { 41 | conn net.Conn 42 | 43 | boxer *boxstream.Boxer 44 | unboxer *boxstream.Unboxer 45 | recvMsg []byte // last message read from unboxer 46 | 47 | // public keys 48 | local, remote []byte 49 | } 50 | 51 | // Read implements io.Reader. 52 | func (conn *Conn) Read(p []byte) (int, error) { 53 | if len(conn.recvMsg) == 0 { 54 | msg, err := conn.unboxer.ReadMessage() 55 | if err != nil { 56 | return 0, err 57 | } 58 | conn.recvMsg = msg 59 | } 60 | n := copy(p, conn.recvMsg) 61 | conn.recvMsg = conn.recvMsg[n:] 62 | return n, nil 63 | } 64 | 65 | // Write implements io.Writer. 66 | func (conn *Conn) Write(p []byte) (int, error) { 67 | for buf := bytes.NewBuffer(p); buf.Len() > 0; { 68 | if err := conn.boxer.WriteMessage(buf.Next(boxstream.MaxSegmentSize)); err != nil { 69 | return 0, err 70 | } 71 | } 72 | return len(p), nil 73 | } 74 | 75 | // Close closes the underlying net.Conn 76 | func (conn *Conn) Close() error { 77 | gerr := conn.boxer.WriteGoodbye() 78 | if gerr != nil { 79 | netErr := new(net.OpError) 80 | if errors.As(gerr, &netErr) { 81 | var sysCallErr = new(os.SyscallError) 82 | if errors.As(netErr.Err, &sysCallErr) { 83 | action := sysCallErr.Unwrap() 84 | if action == syscall.ECONNRESET || action == syscall.EPIPE { 85 | return nil 86 | } 87 | } 88 | if netErr.Err.Error() == "use of closed network connection" { 89 | return nil 90 | } 91 | } 92 | return gerr 93 | } 94 | 95 | if cerr := conn.conn.Close(); cerr != nil { 96 | return cerr 97 | } 98 | 99 | return nil 100 | } 101 | 102 | // LocalAddr returns the local net.Addr with the local public key 103 | func (conn *Conn) LocalAddr() net.Addr { 104 | return netwrap.WrapAddr(conn.conn.LocalAddr(), Addr{conn.local}) 105 | } 106 | 107 | // RemoteAddr returns the remote net.Addr with the remote public key 108 | func (conn *Conn) RemoteAddr() net.Addr { 109 | return netwrap.WrapAddr(conn.conn.RemoteAddr(), Addr{conn.remote}) 110 | } 111 | 112 | // SetDeadline passes the call to the underlying net.Conn 113 | func (conn *Conn) SetDeadline(t time.Time) error { 114 | return conn.conn.SetDeadline(t) 115 | } 116 | 117 | // SetReadDeadline passes the call to the underlying net.Conn 118 | func (conn *Conn) SetReadDeadline(t time.Time) error { 119 | return conn.conn.SetReadDeadline(t) 120 | } 121 | 122 | // SetWriteDeadline passes the call to the underlying net.Conn 123 | func (conn *Conn) SetWriteDeadline(t time.Time) error { 124 | return conn.conn.SetWriteDeadline(t) 125 | } 126 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | go 1.13 6 | 7 | module github.com/ssbc/go-secretstream 8 | 9 | require ( 10 | filippo.io/edwards25519 v1.0.0-rc.1 11 | github.com/ssbc/go-netwrap v0.1.5-0.20221019160355-cd323bb2e29d 12 | github.com/stretchr/testify v1.4.0 13 | go.mindeco.de v1.12.0 14 | golang.org/x/crypto v0.17.0 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= 2 | filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= 3 | github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= 4 | github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 9 | github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= 10 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 11 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 12 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 13 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 14 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 15 | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 16 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= 17 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 18 | github.com/miolini/datacounter v0.0.0-20171104152933-fd4e42a1d5e0/go.mod h1:P6fDJzlxN+cWYR09KbE9/ta+Y6JofX9tAUhJpWkWPaM= 19 | github.com/oxtoacart/bpool v0.0.0-20190524125616-8c0b41497736/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0= 20 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 21 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/shurcooL/httpfs v0.0.0-20190527155220-6a4d4a70508b/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 25 | github.com/ssbc/go-netwrap v0.1.5-0.20221019160355-cd323bb2e29d h1:UnYPPekKU0mHzMMOSuI6117Djq9xni60c/IzzUYxgCI= 26 | github.com/ssbc/go-netwrap v0.1.5-0.20221019160355-cd323bb2e29d/go.mod h1:tsE1qeqkc8kvf1psPNdJ5s8O+/jE1WlKwsEETb2VZqs= 27 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 28 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 29 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 30 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 31 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 32 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 33 | go.mindeco.de v1.12.0 h1:K5FHILjJlD/U1HJMs8Y9ZLwdfG4dPEsxw+e+eqg1wKc= 34 | go.mindeco.de v1.12.0/go.mod h1:dZty08izAk/rSX8wSLen4gMR4WDPYmA6vUTE0QtepHA= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 37 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 38 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 39 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 40 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 41 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 42 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 43 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 44 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 45 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 47 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 48 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 49 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 50 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 51 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 52 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 56 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 57 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 67 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 69 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 70 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 71 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 72 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 76 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 77 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 78 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 79 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 80 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 81 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 82 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 83 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 84 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 86 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 88 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 89 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 90 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 91 | -------------------------------------------------------------------------------- /go.sum.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /internal/lo25519/ed25519.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package lo25519 6 | 7 | // edBlacklist is a list of elements of the ed25519 curve that have low order. 8 | // The list was copied from https://github.com/jedisct1/libsodium/blob/141288535127c22162944e12fcadb8bc269671cc/src/libsodium/crypto_core/ed25519/ref10/ed25519_ref10.c 9 | var edBlacklist = [7][32]byte{ 10 | /* 0 (order 4) */ 11 | {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 12 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 13 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 14 | /* 1 (order 1) */ 15 | {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 18 | /* 2707385501144840649318225287225658788936804267575313519463743609750303402022 19 | (order 8) */ 20 | {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 21 | 0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 22 | 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05}, 23 | /* 55188659117513257062467267217118295137698188065244968500265048394206261417927 24 | (order 8) */ 25 | {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 26 | 0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 27 | 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a}, 28 | /* p-1 (order 2) */ 29 | {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 30 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 31 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, 32 | /* p (=0, order 4) */ 33 | {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 34 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 35 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, 36 | /* p+1 (=1, order 1) */ 37 | {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 38 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 39 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, 40 | } 41 | 42 | // IsLowOrder checks if the passed group element is of low order. 43 | // Algorithm translated from the same source as the blacklist (see above). 44 | func IsEdLowOrder(ge []byte) bool { 45 | var ( 46 | c [7]byte 47 | k int 48 | i, j int 49 | ) 50 | 51 | // cases j = 0..30 52 | for j = 0; j < 31; j++ { 53 | for i = 0; i < len(edBlacklist); i++ { 54 | c[i] |= ge[j] ^ edBlacklist[i][j] 55 | } 56 | } 57 | 58 | // case j = 31, ignore highest bit 59 | for i = 0; i < len(edBlacklist); i++ { 60 | c[i] |= (ge[j] & 0x7f) ^ edBlacklist[i][j] 61 | } 62 | 63 | k = 0 64 | for i = 0; i < len(edBlacklist); i++ { 65 | k |= int(c[i]) - 1 66 | } 67 | 68 | return ((k >> 8) & 1) == 1 69 | } 70 | -------------------------------------------------------------------------------- /net_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secretstream 6 | 7 | import ( 8 | "bytes" 9 | "encoding/base64" 10 | "fmt" 11 | "io" 12 | "math/rand" 13 | "net" 14 | "strings" 15 | "sync" 16 | "testing" 17 | 18 | "github.com/ssbc/go-netwrap" 19 | "github.com/ssbc/go-secretstream/secrethandshake" 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | var ( 24 | clientKeys, serverKeys *secrethandshake.EdKeyPair 25 | 26 | appKey []byte 27 | ) 28 | 29 | func init() { 30 | var err error 31 | clientKeys, err = secrethandshake.GenEdKeyPair(nil) 32 | check(err) 33 | serverKeys, err = secrethandshake.GenEdKeyPair(nil) 34 | check(err) 35 | 36 | appKey, err = base64.StdEncoding.DecodeString("UjFLJ+aDSwKlaxxLBA3aWfL0pJDbrERwF1MWzQbeD0A=") 37 | check(err) 38 | } 39 | 40 | func check(err error) { 41 | if err != nil { 42 | panic(err) 43 | } 44 | } 45 | 46 | func mkCheck(errc chan<- error) func(err error) { 47 | return func(err error) { 48 | if err != nil { 49 | errc <- err 50 | } 51 | } 52 | } 53 | 54 | func mergedErrors(cs ...<-chan error) <-chan error { 55 | var wg sync.WaitGroup 56 | out := make(chan error, 1) 57 | 58 | output := func(c <-chan error) { 59 | for a := range c { 60 | out <- a 61 | } 62 | wg.Done() 63 | } 64 | 65 | wg.Add(len(cs)) 66 | for _, c := range cs { 67 | go output(c) 68 | } 69 | 70 | go func() { 71 | wg.Wait() 72 | close(out) 73 | }() 74 | return out 75 | } 76 | 77 | func TestNet(t *testing.T) { 78 | r := require.New(t) 79 | 80 | s, err := NewServer(*serverKeys, appKey) 81 | r.NoError(err) 82 | 83 | l, err := netwrap.Listen(&net.TCPAddr{IP: net.IP{127, 0, 0, 1}}, s.ListenerWrapper()) 84 | r.NoError(err) 85 | 86 | testData := strings.Repeat("Hello, World!", 50) 87 | 88 | srvErrc := make(chan error) 89 | check := mkCheck(srvErrc) 90 | go func() { 91 | var ( 92 | conn net.Conn 93 | err error 94 | ) 95 | conn, err = l.Accept() 96 | check(err) 97 | 98 | _, err = conn.Write(appKey) 99 | check(err) 100 | 101 | buf := make([]byte, len(testData)) 102 | _, err = io.ReadFull(conn, buf) 103 | check(err) 104 | 105 | if string(buf) != testData { 106 | fmt.Errorf("server read wrong bytes: %x", buf) 107 | return 108 | } 109 | 110 | check(conn.Close()) 111 | check(l.Close()) 112 | close(srvErrc) 113 | }() 114 | 115 | c, err := NewClient(*clientKeys, appKey) 116 | r.NoError(err) 117 | 118 | tcpAddr := netwrap.GetAddr(l.Addr(), "tcp") 119 | connWrap := c.ConnWrapper(serverKeys.Public) 120 | 121 | conn, err := netwrap.Dial(tcpAddr, connWrap) 122 | r.NoError(err) 123 | 124 | buf := make([]byte, len(appKey)) 125 | _, err = io.ReadFull(conn, buf) 126 | r.NoError(err) 127 | 128 | if !bytes.Equal(buf, appKey) { 129 | t.Fatalf("client read wrong bytes - expected %q, got %q", appKey, buf) 130 | } 131 | 132 | _, err = conn.Write([]byte(testData)) 133 | r.NoError(err) 134 | 135 | r.NoError(conn.Close(), "failed to close conn") 136 | 137 | i := 0 138 | for e := range srvErrc { 139 | r.NoError(e, "err %d from chan", i) 140 | i++ 141 | } 142 | 143 | } 144 | 145 | func TestNetClose(t *testing.T) { 146 | r := require.New(t) 147 | 148 | s, err := NewServer(*serverKeys, appKey) 149 | r.NoError(err) 150 | 151 | l, err := netwrap.Listen(&net.TCPAddr{IP: net.IP{127, 0, 0, 1}}, s.ListenerWrapper()) 152 | r.NoError(err) 153 | 154 | // 1 MiB 155 | testData := make([]byte, 1024*1024) 156 | for i, _ := range testData { 157 | testData[i] = byte(rand.Int() % 255) 158 | } 159 | 160 | srvErrc := make(chan error) 161 | check := mkCheck(srvErrc) 162 | go func() { 163 | var ( 164 | c net.Conn 165 | err error 166 | ) 167 | c, err = l.Accept() 168 | check(err) 169 | 170 | _, err = c.Write(testData) 171 | check(err) 172 | // Immediately close conn after Write() 173 | 174 | check(c.Close()) 175 | check(l.Close()) 176 | close(srvErrc) 177 | }() 178 | c, err := NewClient(*clientKeys, appKey) 179 | r.NoError(err) 180 | 181 | client, err := netwrap.Dial(netwrap.GetAddr(l.Addr(), "tcp"), c.ConnWrapper(serverKeys.Public)) 182 | r.NoError(err) 183 | 184 | recData := make([]byte, 1024*1024) 185 | _, err = io.ReadFull(client, recData) 186 | r.NoError(err) 187 | r.Equal(recData, testData, "client read wrong bytes") 188 | 189 | r.NoError(client.Close(), "failed to close client") 190 | 191 | i := 0 192 | for e := range srvErrc { 193 | r.NoError(e, "err %d from chan", i) 194 | i++ 195 | } 196 | } 197 | 198 | // a concurrent write might produce a race on the nonce 199 | func TestRaceClose(t *testing.T) { 200 | r := require.New(t) 201 | 202 | s, err := NewServer(*serverKeys, appKey) 203 | r.NoError(err) 204 | 205 | l, err := netwrap.Listen(&net.TCPAddr{IP: net.IP{127, 0, 0, 1}}, s.ListenerWrapper()) 206 | r.NoError(err) 207 | 208 | // 1 MiB 209 | testData := make([]byte, 1024*1024) 210 | for i, _ := range testData { 211 | testData[i] = byte(rand.Int() % 255) 212 | } 213 | 214 | srvErrc := make(chan error, 1) 215 | 216 | srving := make(chan net.Conn) 217 | go func() { 218 | check := mkCheck(srvErrc) 219 | c, err := l.Accept() 220 | check(err) 221 | 222 | // check(l.Close()) // one connection is enough 223 | srving <- c // give the conn to a _connection pool_ 224 | 225 | // write one byte at a time, just to make this case more likely 226 | for b := bytes.NewBuffer(testData); b.Len() > 0; { 227 | _, err = c.Write(b.Next(1)) 228 | if oerr, ok := err.(*net.OpError); ok { 229 | if oerr.Err.Error() == "use of closed network connection" { 230 | close(srvErrc) 231 | return 232 | } 233 | } 234 | check(err) 235 | } 236 | 237 | check(c.Close()) 238 | close(srvErrc) 239 | }() 240 | 241 | // another part of the stack might decide to close it as it's being used 242 | closeErrc := make(chan error, 1) 243 | go func() { 244 | check := mkCheck(closeErrc) 245 | c := <-srving 246 | check(c.Close()) 247 | close(closeErrc) 248 | }() 249 | 250 | c, err := NewClient(*clientKeys, appKey) 251 | r.NoError(err) 252 | 253 | client, err := netwrap.Dial(netwrap.GetAddr(l.Addr(), "tcp"), c.ConnWrapper(serverKeys.Public)) 254 | r.NoError(err) 255 | 256 | recData := make([]byte, 1024*1024) 257 | _, err = io.ReadFull(client, recData) 258 | r.Error(err) 259 | 260 | err = client.Close() 261 | if err != nil { 262 | t.Error("failed to close client", err) 263 | } 264 | 265 | i := 0 266 | for e := range mergedErrors(srvErrc, closeErrc) { 267 | r.NoError(e, "err %d from chan", i) 268 | i++ 269 | } 270 | } 271 | 272 | func TestNetCloseEarly(t *testing.T) { 273 | r := require.New(t) 274 | 275 | s, err := NewServer(*serverKeys, appKey) 276 | r.NoError(err) 277 | 278 | l, err := netwrap.Listen(&net.TCPAddr{IP: net.IP{127, 0, 0, 1}}, s.ListenerWrapper()) 279 | r.NoError(err) 280 | 281 | // 1 MiB 282 | testData := make([]byte, 1024*1024) 283 | for i, _ := range testData { 284 | testData[i] = byte(rand.Int() % 255) 285 | } 286 | 287 | srvErrc := make(chan error) 288 | check := mkCheck(srvErrc) 289 | go func() { 290 | var ( 291 | c net.Conn 292 | err error 293 | ) 294 | c, err = l.Accept() 295 | check(err) 296 | 297 | check(l.Close()) // one connection is enough 298 | 299 | // short write 300 | _, err = c.Write(testData[:10]) 301 | check(err) 302 | 303 | check(c.Close()) 304 | close(srvErrc) 305 | }() 306 | c, err := NewClient(*clientKeys, appKey) 307 | r.NoError(err) 308 | 309 | client, err := netwrap.Dial(netwrap.GetAddr(l.Addr(), "tcp"), c.ConnWrapper(serverKeys.Public)) 310 | r.NoError(err) 311 | 312 | recData := make([]byte, 1024*1024) 313 | _, err = io.ReadFull(client, recData) 314 | r.Error(err) 315 | 316 | err = client.Close() 317 | if err != nil { 318 | t.Error("failed to close client", err) 319 | } 320 | 321 | i := 0 322 | for e := range srvErrc { 323 | r.NoError(e, "err %d from chan", i) 324 | i++ 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /secrethandshake/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | node_modules -------------------------------------------------------------------------------- /secrethandshake/client_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | //go:build interop_nodejs 6 | // +build interop_nodejs 7 | 8 | package secrethandshake 9 | 10 | import ( 11 | "encoding/base64" 12 | "testing" 13 | 14 | "go.mindeco.de/logging/logtest" 15 | "go.mindeco.de/proc" 16 | ) 17 | 18 | func TestClient(t *testing.T) { 19 | w := logtest.Logger("server_test.js", t) 20 | server, err := proc.StartStdioProcess("node", w, "server_test.js") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | appKey, err := base64.StdEncoding.DecodeString("IhrX11txvFiVzm+NurzHLCqUUe3xZXkPfODnp7WlMpk=") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | kpBob := mustLoadTestKeyPair(t, "key.bob.json") 31 | kpAlice := mustLoadTestKeyPair(t, "key.alice.json") 32 | 33 | clientState, err := NewClientState(appKey, kpAlice, kpBob.Public) 34 | if err != nil { 35 | t.Fatal("error making server state:", err) 36 | } 37 | 38 | if err := Client(clientState, server); err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | if err := server.Close(); err != nil { 43 | t.Fatal(err) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /secrethandshake/client_test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | var shs = require('secret-handshake') 6 | var fs = require('fs') 7 | var pull = require('pull-stream') 8 | var toPull = require('stream-to-pull-stream') 9 | 10 | function readKeyF (fname) { 11 | var tmpobj = JSON.parse(fs.readFileSync(fname).toString()) 12 | return { 13 | publicKey: Buffer.from(tmpobj.publicKey, 'base64'), 14 | secretKey: Buffer.from(tmpobj.secretKey, 'base64') 15 | } 16 | } 17 | 18 | var alice = readKeyF('key.alice.json') 19 | var bob = readKeyF('key.bob.json') 20 | 21 | var createClient = shs.createClient(alice, Buffer.from('IhrX11txvFiVzm+NurzHLCqUUe3xZXkPfODnp7WlMpk=', 'base64')) 22 | 23 | pull( 24 | toPull.source(process.stdin), 25 | createClient(bob.publicKey, function () { }), 26 | toPull.sink(process.stdout) 27 | ) 28 | -------------------------------------------------------------------------------- /secrethandshake/conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secrethandshake 6 | 7 | import ( 8 | "crypto/rand" 9 | "io" 10 | 11 | "github.com/ssbc/go-secretstream/internal/lo25519" 12 | "golang.org/x/crypto/ed25519" 13 | ) 14 | 15 | // ChallengeLength is the length of a challenge message in bytes 16 | const ChallengeLength = 64 17 | 18 | // ClientAuthLength is the length of a clientAuth message in bytes 19 | const ClientAuthLength = 16 + 32 + 64 20 | 21 | // ServerAuthLength is the length of a serverAuth message in bytes 22 | const ServerAuthLength = 16 + 64 23 | 24 | // MACLength is the length of a MAC in bytes 25 | const MACLength = 16 26 | 27 | // GenEdKeyPair generates a ed25519 keyPair using the passed reader 28 | // if r == nil it uses crypto/rand.Reader 29 | func GenEdKeyPair(r io.Reader) (*EdKeyPair, error) { 30 | if r == nil { 31 | r = rand.Reader 32 | } 33 | pubSrv, secSrv, err := ed25519.GenerateKey(r) 34 | if err != nil { 35 | return nil, err 36 | } 37 | if lo25519.IsEdLowOrder(pubSrv[:]) { 38 | pubSrv, secSrv, err = ed25519.GenerateKey(r) 39 | } 40 | return &EdKeyPair{pubSrv, secSrv}, nil 41 | } 42 | 43 | // Client shakes hands using the cryptographic identity specified in s using conn in the client role 44 | func Client(state *State, conn io.ReadWriter) (err error) { 45 | // send challenge 46 | _, err = conn.Write(state.createChallenge()) 47 | if err != nil { 48 | return ErrProcessing{where: "sending challenge", cause: err} 49 | } 50 | 51 | // recv challenge 52 | chalResp := make([]byte, ChallengeLength) 53 | _, err = io.ReadFull(conn, chalResp) 54 | if err != nil { 55 | return ErrProcessing{where: "receiving challenge", cause: err} 56 | } 57 | 58 | // verify challenge 59 | if !state.verifyChallenge(chalResp) { 60 | return ErrProtocol{0} 61 | } 62 | 63 | // send authentication vector 64 | _, err = conn.Write(state.createClientAuth()) 65 | if err != nil { 66 | return ErrProcessing{where: "sending client hello", cause: err} 67 | } 68 | 69 | // recv authentication vector 70 | boxedSig := make([]byte, ServerAuthLength) 71 | _, err = io.ReadFull(conn, boxedSig) 72 | if err != nil { 73 | return ErrProcessing{where: "receiving server auth", cause: err} 74 | } 75 | 76 | // authenticate remote 77 | if !state.verifyServerAccept(boxedSig) { 78 | return ErrProtocol{1} 79 | } 80 | 81 | state.cleanSecrets() 82 | return nil 83 | } 84 | 85 | // Server shakes hands using the cryptographic identity specified in s using conn in the server role 86 | func Server(state *State, conn io.ReadWriter) (err error) { 87 | // recv challenge 88 | challenge := make([]byte, ChallengeLength) 89 | _, err = io.ReadFull(conn, challenge) 90 | if err != nil { 91 | return ErrProcessing{where: "receiving challenge", cause: err} 92 | } 93 | 94 | // verify challenge 95 | if !state.verifyChallenge(challenge) { 96 | return ErrProtocol{0} 97 | } 98 | 99 | // send challenge 100 | _, err = conn.Write(state.createChallenge()) 101 | if err != nil { 102 | return ErrProcessing{where: "sending challenge", cause: err} 103 | } 104 | 105 | // recv authentication vector 106 | hello := make([]byte, ClientAuthLength) 107 | _, err = io.ReadFull(conn, hello) 108 | if err != nil { 109 | return ErrProcessing{where: "receiving client hello", cause: err} 110 | } 111 | 112 | // authenticate remote 113 | if !state.verifyClientAuth(hello) { 114 | return ErrProtocol{1} 115 | } 116 | 117 | // accept 118 | _, err = conn.Write(state.createServerAccept()) 119 | if err != nil { 120 | return ErrProcessing{where: "sending server accept", cause: err} 121 | } 122 | 123 | state.cleanSecrets() 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /secrethandshake/conn_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secrethandshake 6 | 7 | import ( 8 | "io" 9 | "log" 10 | "os" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | // StupidRandom always reads itself. Goal is determinism. 16 | type StupidRandom byte 17 | 18 | // Read reads from the stupid random source 19 | func (sr StupidRandom) Read(buf []byte) (int, error) { 20 | for i := range buf { 21 | buf[i] = byte(sr) 22 | } 23 | 24 | return len(buf), nil 25 | } 26 | 27 | // rw implements io.ReadWriter using an io.Reader and io.Writer 28 | type rw struct { 29 | io.Reader 30 | io.Writer 31 | } 32 | 33 | // TestAuth is an integration test 34 | func TestAuth(t *testing.T) { 35 | log.SetOutput(os.Stdout) 36 | 37 | keySrv, err := GenEdKeyPair(StupidRandom(0)) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | keyClient, err := GenEdKeyPair(StupidRandom(1)) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | appKey := make([]byte, 32) 48 | io.ReadFull(StupidRandom(255), appKey) 49 | 50 | rServer, wClient := io.Pipe() 51 | rClient, wServer := io.Pipe() 52 | 53 | rwServer := rw{rServer, wServer} 54 | rwClient := rw{rClient, wClient} 55 | 56 | serverState, err := NewServerState(appKey, *keySrv) 57 | if err != nil { 58 | t.Error("error making server state:", err) 59 | } 60 | 61 | clientState, err := NewClientState(appKey, *keyClient, keySrv.Public) 62 | if err != nil { 63 | t.Error("error making client state:", err) 64 | } 65 | 66 | // buffered channel 67 | ch := make(chan error, 2) 68 | 69 | go func() { 70 | err := Server(serverState, rwServer) 71 | ch <- err 72 | wServer.Close() 73 | }() 74 | 75 | go func() { 76 | err := Client(clientState, rwClient) 77 | ch <- err 78 | wClient.Close() 79 | }() 80 | 81 | // t.Error may only be called from this goroutine :/ 82 | if err = <-ch; err != nil { 83 | t.Errorf("1st ch read: %v", err) 84 | } 85 | if err = <-ch; err != nil { 86 | t.Errorf("2nd ch read: %v", err) 87 | } 88 | 89 | if !reflect.DeepEqual(clientState.secret, serverState.secret) { 90 | t.Error("secrets not equal") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /secrethandshake/errors.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secrethandshake 6 | 7 | import ( 8 | "fmt" 9 | "strconv" 10 | ) 11 | 12 | var ErrInvalidKeyPair = fmt.Errorf("secrethandshake/NewKeyPair: invalid public key") 13 | 14 | type ErrKeySize struct { 15 | tipe string 16 | n int 17 | } 18 | 19 | func (eks ErrKeySize) Error() string { 20 | return fmt.Sprintf("secrethandshake/NewKeyPair: invalid size (%d) for %s key", eks.n, eks.tipe) 21 | } 22 | 23 | type ErrProtocol struct{ code int } 24 | 25 | func (e ErrProtocol) Error() string { 26 | switch e.code { 27 | case 0: 28 | return "secrethandshake: Wrong protocol version?" 29 | case 1: 30 | return "secrethandshake: other side not authenticated" 31 | default: 32 | 33 | return "secrethandshake: unhandled protocol error " + strconv.Itoa(e.code) 34 | } 35 | } 36 | 37 | // ErrProcessing is returned if I/O fails during the handshake 38 | // TODO: supply Unwrap() for cause? 39 | type ErrProcessing struct { 40 | where string 41 | cause error 42 | } 43 | 44 | func (e ErrProcessing) Error() string { 45 | errStr := "secrethandshake: failed during data transfer of " + e.where 46 | errStr += ": " + e.cause.Error() 47 | return errStr 48 | } 49 | 50 | // Unwrap returns the cause 51 | func (e ErrProcessing) Unwrap() error { return e.cause } 52 | 53 | type ErrEncoding struct { 54 | what string 55 | cause error 56 | } 57 | 58 | func (e ErrEncoding) Error() string { 59 | errStr := "secrethandshake: failed during encoding task of " + e.what 60 | errStr += ": " + e.cause.Error() 61 | return errStr 62 | } 63 | 64 | // Unwrap returns the cause 65 | func (e ErrEncoding) Unwrap() error { return e.cause } 66 | -------------------------------------------------------------------------------- /secrethandshake/errors_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secrethandshake 6 | 7 | import ( 8 | "errors" 9 | "io" 10 | "testing" 11 | ) 12 | 13 | func TestUnwrapErr(t *testing.T) { 14 | 15 | var procErr = ErrProcessing{ 16 | where: "test", 17 | cause: io.EOF, 18 | } 19 | 20 | unwrapped := errors.Unwrap(procErr) 21 | if unwrapped != io.EOF { 22 | t.Error("does not unwrap to EOF, got:", unwrapped) 23 | } 24 | 25 | if !errors.Is(procErr, io.EOF) { 26 | t.Error("errors.Is(err,eof) not true") 27 | } 28 | 29 | var encErr = ErrEncoding{ 30 | what: "test enc", 31 | cause: io.ErrUnexpectedEOF, 32 | } 33 | 34 | unwrapped = errors.Unwrap(encErr) 35 | if unwrapped != io.ErrUnexpectedEOF { 36 | t.Error("not unexpected EOF, got:", unwrapped) 37 | } 38 | 39 | if !errors.Is(encErr, io.ErrUnexpectedEOF) { 40 | t.Error("errors.Is(err,unexpected EOF) not true") 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /secrethandshake/genkey.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | var sodium = require('chloride') 6 | var k = sodium.crypto_sign_keypair() 7 | 8 | console.log(JSON.stringify({ 9 | "publicKey":k.publicKey.toString("base64"), 10 | "secretKey":k.secretKey.toString("base64"), 11 | })) 12 | 13 | -------------------------------------------------------------------------------- /secrethandshake/helpers_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secrethandshake 6 | 7 | import ( 8 | "encoding/base64" 9 | "encoding/json" 10 | "os" 11 | "testing" 12 | ) 13 | 14 | func checker(t *testing.T) func(err error) { 15 | return func(err error) { 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | } 20 | } 21 | 22 | func mustLoadTestKeyPair(t *testing.T, fname string) EdKeyPair { 23 | check := checker(t) 24 | 25 | f, err := os.Open(fname) 26 | check(err) 27 | defer f.Close() 28 | 29 | var keyPair struct { 30 | PublicKey, SecretKey string 31 | } 32 | check(json.NewDecoder(f).Decode(&keyPair)) 33 | 34 | var kp EdKeyPair 35 | kp.Public, err = base64.StdEncoding.DecodeString(keyPair.PublicKey) 36 | check(err) 37 | kp.Secret, err = base64.StdEncoding.DecodeString(keyPair.SecretKey) 38 | check(err) 39 | 40 | return kp 41 | } 42 | -------------------------------------------------------------------------------- /secrethandshake/internal/extra25519/convert.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | // Package extra25519 implements the key conversion from ed25519 to curve25519. Nothing more. 6 | package extra25519 7 | 8 | import ( 9 | "crypto/sha512" 10 | 11 | "filippo.io/edwards25519" 12 | "golang.org/x/crypto/ed25519" 13 | 14 | "github.com/ssbc/go-secretstream/internal/lo25519" 15 | ) 16 | 17 | // PrivateKeyToCurve25519 converts an ed25519 private key into a corresponding 18 | // curve25519 private key such that the resulting curve25519 public key will 19 | // equal the result from PublicKeyToCurve25519. 20 | func PrivateKeyToCurve25519(curve25519Private *[32]byte, privateKey ed25519.PrivateKey) { 21 | h := sha512.New() 22 | h.Write(privateKey[:32]) 23 | digest := h.Sum(nil) 24 | 25 | digest[0] &= 248 26 | digest[31] &= 127 27 | digest[31] |= 64 28 | 29 | copy(curve25519Private[:], digest) 30 | } 31 | 32 | // PublicKeyToCurve25519 converts an Ed25519 public key into the curve25519 33 | // public key that would be generated from the same private key. 34 | func PublicKeyToCurve25519(curveBytes *[32]byte, edBytes ed25519.PublicKey) bool { 35 | if lo25519.IsEdLowOrder(edBytes) { 36 | return false 37 | } 38 | 39 | edPoint, err := new(edwards25519.Point).SetBytes(edBytes) 40 | if err != nil { 41 | return false 42 | } 43 | 44 | copy(curveBytes[:], edPoint.BytesMontgomery()) 45 | return true 46 | } 47 | -------------------------------------------------------------------------------- /secrethandshake/internal/extra25519/convert_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package extra25519_test 6 | 7 | import ( 8 | "bytes" 9 | 10 | "github.com/ssbc/go-secretstream/secrethandshake/internal/extra25519" 11 | "golang.org/x/crypto/ed25519" 12 | 13 | "encoding/hex" 14 | "fmt" 15 | "os" 16 | ) 17 | 18 | func ExamplePrivateKeyToCurve25519() { 19 | dumper := hex.Dumper(os.Stdout) 20 | 21 | // really silly seed for reproduciability 22 | seed := make([]byte, 32) 23 | 24 | fmt.Println("seed:") 25 | dumper.Write(seed) 26 | 27 | _, private, err := ed25519.GenerateKey(bytes.NewReader(seed)) 28 | fatal(err) 29 | 30 | fmt.Println("private ed25519:") 31 | dumper.Write(private) 32 | 33 | var curvPriv [32]byte 34 | extra25519.PrivateKeyToCurve25519(&curvPriv, private) 35 | 36 | fmt.Println("private curve25519:") 37 | dumper.Write(curvPriv[:]) 38 | 39 | // Output: 40 | // seed: 41 | // 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 42 | // 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 43 | // private ed25519: 44 | // 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 45 | // 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 46 | // 00000040 3b 6a 27 bc ce b6 a4 2d 62 a3 a8 d0 2a 6f 0d 73 |;j'....-b...*o.s| 47 | // 00000050 65 32 15 77 1d e2 43 a6 3a c0 48 a1 8b 59 da 29 |e2.w..C.:.H..Y.)| 48 | // private curve25519: 49 | // 00000060 50 46 ad c1 db a8 38 86 7b 2b bb fd d0 c3 42 3e |PF....8.{+....B>| 50 | // 00000070 58 b5 79 70 b5 26 7a 90 f5 79 60 92 4a 87 f1 56 |X.yp.&z..y`.J..V| 51 | } 52 | 53 | func ExamplePublicKeyToCurve25519() { 54 | dumper := hex.Dumper(os.Stdout) 55 | 56 | // really silly seed for reproduciability 57 | seed := make([]byte, 32) 58 | 59 | fmt.Println("seed:") 60 | dumper.Write(seed) 61 | 62 | public, _, err := ed25519.GenerateKey(bytes.NewReader(seed)) 63 | fatal(err) 64 | 65 | fmt.Println("public ed25519:") 66 | dumper.Write(public) 67 | 68 | var curvPub [32]byte 69 | extra25519.PublicKeyToCurve25519(&curvPub, public) 70 | 71 | fmt.Println("public curve25519:") 72 | dumper.Write(curvPub[:]) 73 | 74 | // Output: 75 | // seed: 76 | // 00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 77 | // 00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 78 | // public ed25519: 79 | // 00000020 3b 6a 27 bc ce b6 a4 2d 62 a3 a8 d0 2a 6f 0d 73 |;j'....-b...*o.s| 80 | // 00000030 65 32 15 77 1d e2 43 a6 3a c0 48 a1 8b 59 da 29 |e2.w..C.:.H..Y.)| 81 | // public curve25519: 82 | // 00000040 5b f5 5c 73 b8 2e be 22 be 80 f3 43 06 67 af 57 |[.\s..."...C.g.W| 83 | // 00000050 0f ae 25 56 a6 41 5e 6b 30 d4 06 53 00 aa 94 7d |..%V.A^k0..S...}| 84 | } 85 | 86 | func fatal(err error) { 87 | if err != nil { 88 | panic(err) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /secrethandshake/key.alice.json: -------------------------------------------------------------------------------- 1 | {"publicKey":"reZVj+irIL9Iev8xDh9R2jiPzgkM3QOYB158oxt2TT8=","secretKey":"XRCnlde+L/QoBpLawuwVRqk6biMeHY5P4fOgA4G1Du+t5lWP6Ksgv0h6/zEOH1HaOI/OCQzdA5gHXnyjG3ZNPw=="} 2 | -------------------------------------------------------------------------------- /secrethandshake/key.alice.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /secrethandshake/key.bob.json: -------------------------------------------------------------------------------- 1 | {"publicKey":"dz9tpZBrNTCCKiA+mIn/x1M6IEO9k05DbkgpqsGbTpE=","secretKey":"kB4WbT+KvUYwpCxc0FC5+FYKFbUFMoIR+f7VhDVWvU13P22lkGs1MIIqID6Yif/HUzogQ72TTkNuSCmqwZtOkQ=="} 2 | -------------------------------------------------------------------------------- /secrethandshake/key.bob.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /secrethandshake/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "chloride": { 6 | "version": "2.4.1", 7 | "resolved": "https://registry.npmjs.org/chloride/-/chloride-2.4.1.tgz", 8 | "integrity": "sha512-ZiID87W2o2llvuF4C7Fvt9GJisazSdMsSkjAq4WaMed9zn77nlkcy08ZfrPtOGAXyaxTDj0VjnuyD97EdJLz3g==", 9 | "requires": { 10 | "sodium-browserify": "^1.2.7", 11 | "sodium-browserify-tweetnacl": "^0.2.5", 12 | "sodium-chloride": "^1.1.2", 13 | "sodium-native": "^3.0.0" 14 | } 15 | }, 16 | "chloride-test": { 17 | "version": "1.2.4", 18 | "resolved": "https://registry.npmjs.org/chloride-test/-/chloride-test-1.2.4.tgz", 19 | "integrity": "sha512-9vhoi1qXSBPn6//ZxIgSe3M2QhKHzIPZQzmrZgmPADsqW0Jxpe3db1e7aGSRUMXbxAQ04SfypdT8dGaSvIvKDw==", 20 | "requires": { 21 | "json-buffer": "^2.0.11" 22 | } 23 | }, 24 | "ed2curve": { 25 | "version": "0.1.4", 26 | "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.1.4.tgz", 27 | "integrity": "sha1-lKRCSLuH2jXbDv968KpXYWgRf1k=", 28 | "requires": { 29 | "tweetnacl": "0.x.x" 30 | } 31 | }, 32 | "explain-error": { 33 | "version": "1.0.4", 34 | "resolved": "https://registry.npmjs.org/explain-error/-/explain-error-1.0.4.tgz", 35 | "integrity": "sha1-p5PTrAytTGq1cemWj7urbLJTKSk=" 36 | }, 37 | "increment-buffer": { 38 | "version": "1.0.1", 39 | "resolved": "https://registry.npmjs.org/increment-buffer/-/increment-buffer-1.0.1.tgz", 40 | "integrity": "sha1-ZQdtdRidgIs5rROrW5WOBSFvng0=" 41 | }, 42 | "inherits": { 43 | "version": "2.0.4", 44 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 45 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 46 | }, 47 | "ini": { 48 | "version": "1.3.8", 49 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 50 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 51 | "optional": true 52 | }, 53 | "json-buffer": { 54 | "version": "2.0.11", 55 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-2.0.11.tgz", 56 | "integrity": "sha1-PkQf2jCYvo0eMXGtWRvGKjPi1V8=" 57 | }, 58 | "libsodium": { 59 | "version": "0.7.9", 60 | "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.9.tgz", 61 | "integrity": "sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==" 62 | }, 63 | "libsodium-wrappers": { 64 | "version": "0.7.9", 65 | "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz", 66 | "integrity": "sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==", 67 | "requires": { 68 | "libsodium": "^0.7.0" 69 | } 70 | }, 71 | "looper": { 72 | "version": "3.0.0", 73 | "resolved": "https://registry.npmjs.org/looper/-/looper-3.0.0.tgz", 74 | "integrity": "sha1-LvpUw7HLq6m5Su4uWRSwvlf7t0k=" 75 | }, 76 | "node-gyp-build": { 77 | "version": "4.2.3", 78 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", 79 | "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", 80 | "optional": true 81 | }, 82 | "pull-box-stream": { 83 | "version": "1.0.13", 84 | "resolved": "https://registry.npmjs.org/pull-box-stream/-/pull-box-stream-1.0.13.tgz", 85 | "integrity": "sha1-w+JAOY6rP1lRsu0QeMWYi/egork=", 86 | "requires": { 87 | "chloride": "^2.2.7", 88 | "increment-buffer": "~1.0.0", 89 | "pull-reader": "^1.2.5", 90 | "pull-stream": "^3.2.3", 91 | "pull-through": "^1.0.18", 92 | "split-buffer": "~1.0.0" 93 | } 94 | }, 95 | "pull-cat": { 96 | "version": "1.1.11", 97 | "resolved": "https://registry.npmjs.org/pull-cat/-/pull-cat-1.1.11.tgz", 98 | "integrity": "sha1-tkLdElXaN2pwa220+pYvX9t0wxs=" 99 | }, 100 | "pull-handshake": { 101 | "version": "1.1.4", 102 | "resolved": "https://registry.npmjs.org/pull-handshake/-/pull-handshake-1.1.4.tgz", 103 | "integrity": "sha1-YACg/QGIhM39c3JU+Mxgqypjd5E=", 104 | "requires": { 105 | "pull-cat": "^1.1.9", 106 | "pull-pair": "~1.1.0", 107 | "pull-pushable": "^2.0.0", 108 | "pull-reader": "^1.2.3" 109 | } 110 | }, 111 | "pull-pair": { 112 | "version": "1.1.0", 113 | "resolved": "https://registry.npmjs.org/pull-pair/-/pull-pair-1.1.0.tgz", 114 | "integrity": "sha1-fuQnJj/fTaglOXrAoF4atLdL120=" 115 | }, 116 | "pull-pushable": { 117 | "version": "2.2.0", 118 | "resolved": "https://registry.npmjs.org/pull-pushable/-/pull-pushable-2.2.0.tgz", 119 | "integrity": "sha1-Xy867UethpGfAbEqLpnW8b13ZYE=" 120 | }, 121 | "pull-reader": { 122 | "version": "1.3.1", 123 | "resolved": "https://registry.npmjs.org/pull-reader/-/pull-reader-1.3.1.tgz", 124 | "integrity": "sha512-CBkejkE5nX50SiSEzu0Qoz4POTJMS/mw8G6aj3h3M/RJoKgggLxyF0IyTZ0mmpXFlXRcLmLmIEW4xeYn7AeDYw==" 125 | }, 126 | "pull-stream": { 127 | "version": "3.6.14", 128 | "resolved": "https://registry.npmjs.org/pull-stream/-/pull-stream-3.6.14.tgz", 129 | "integrity": "sha512-KIqdvpqHHaTUA2mCYcLG1ibEbu/LCKoJZsBWyv9lSYtPkJPBq8m3Hxa103xHi6D2thj5YXa0TqK3L3GUkwgnew==" 130 | }, 131 | "pull-through": { 132 | "version": "1.0.18", 133 | "resolved": "https://registry.npmjs.org/pull-through/-/pull-through-1.0.18.tgz", 134 | "integrity": "sha1-jdYjFCY+Wc9Qlur7sSeitu8xBzU=", 135 | "requires": { 136 | "looper": "~3.0.0" 137 | } 138 | }, 139 | "safe-buffer": { 140 | "version": "5.2.1", 141 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 142 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 143 | }, 144 | "secret-handshake": { 145 | "version": "1.1.20", 146 | "resolved": "https://registry.npmjs.org/secret-handshake/-/secret-handshake-1.1.20.tgz", 147 | "integrity": "sha512-sDtmZDpibGH2ixj3FOmsC3Z/b08eaB2/KAvy2oSp4qvcGdhatBSfb1RdVpwjQl5c3J83WbBo1HSZ7DBtMu43lA==", 148 | "requires": { 149 | "chloride": "^2.2.8", 150 | "explain-error": "^1.0.4", 151 | "pull-box-stream": "^1.0.13", 152 | "pull-handshake": "^1.1.1", 153 | "pull-stream": "^3.4.5" 154 | } 155 | }, 156 | "sha.js": { 157 | "version": "2.4.5", 158 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz", 159 | "integrity": "sha1-J9Fx78yCoRi5ljn/WBZgJCtQbnw=", 160 | "requires": { 161 | "inherits": "^2.0.1" 162 | } 163 | }, 164 | "sodium-browserify": { 165 | "version": "1.3.0", 166 | "resolved": "https://registry.npmjs.org/sodium-browserify/-/sodium-browserify-1.3.0.tgz", 167 | "integrity": "sha512-1KRS6Oew3X13AIZhbmGF0YBdt2pQdafJMfv83OZHWbzxG92YBBnN8HYx/VKmYB4xCe90eidNaDJWBEFw/o3ahw==", 168 | "requires": { 169 | "libsodium-wrappers": "^0.7.4", 170 | "sha.js": "2.4.5", 171 | "sodium-browserify-tweetnacl": "^0.2.5", 172 | "tweetnacl": "^0.14.1" 173 | } 174 | }, 175 | "sodium-browserify-tweetnacl": { 176 | "version": "0.2.6", 177 | "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.6.tgz", 178 | "integrity": "sha512-ZnEI26hdluilpYY28Xc4rc1ALfmEp2TWihkJX6Mdtw0z9RfHfpZJU7P8DoKbN1HcBdU9aJmguFZs7igE8nLJPg==", 179 | "requires": { 180 | "chloride-test": "^1.1.0", 181 | "ed2curve": "^0.1.4", 182 | "sha.js": "^2.4.8", 183 | "tweetnacl": "^1.0.1", 184 | "tweetnacl-auth": "^0.3.0" 185 | }, 186 | "dependencies": { 187 | "sha.js": { 188 | "version": "2.4.11", 189 | "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", 190 | "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", 191 | "requires": { 192 | "inherits": "^2.0.1", 193 | "safe-buffer": "^5.0.1" 194 | } 195 | }, 196 | "tweetnacl": { 197 | "version": "1.0.3", 198 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", 199 | "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" 200 | } 201 | } 202 | }, 203 | "sodium-chloride": { 204 | "version": "1.1.2", 205 | "resolved": "https://registry.npmjs.org/sodium-chloride/-/sodium-chloride-1.1.2.tgz", 206 | "integrity": "sha512-8AVzr9VHueXqfzfkzUA0aXe/Q4XG3UTmhlP6Pt+HQc5bbAPIJFo7ZIMh9tvn+99QuiMcyDJdYumegGAczl0N+g==" 207 | }, 208 | "sodium-native": { 209 | "version": "3.2.1", 210 | "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.2.1.tgz", 211 | "integrity": "sha512-EgDZ/Z7PxL2kCasKk7wnRkV8W9kvwuIlHuHXAxkQm3FF0MgVsjyLBXGjSRGhjE6u7rhSpk3KaMfFM23bfMysIQ==", 212 | "optional": true, 213 | "requires": { 214 | "ini": "^1.3.5", 215 | "node-gyp-build": "^4.2.0" 216 | } 217 | }, 218 | "split-buffer": { 219 | "version": "1.0.0", 220 | "resolved": "https://registry.npmjs.org/split-buffer/-/split-buffer-1.0.0.tgz", 221 | "integrity": "sha1-t+jgq1E0UVi3LB9tvvJAbVHx0Cc=" 222 | }, 223 | "stream-to-pull-stream": { 224 | "version": "1.7.3", 225 | "resolved": "https://registry.npmjs.org/stream-to-pull-stream/-/stream-to-pull-stream-1.7.3.tgz", 226 | "integrity": "sha512-6sNyqJpr5dIOQdgNy/xcDWwDuzAsAwVzhzrWlAPAQ7Lkjx/rv0wgvxEyKwTq6FmNd5rjTrELt/CLmaSw7crMGg==", 227 | "dev": true, 228 | "requires": { 229 | "looper": "^3.0.0", 230 | "pull-stream": "^3.2.3" 231 | } 232 | }, 233 | "tweetnacl": { 234 | "version": "0.14.5", 235 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 236 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 237 | }, 238 | "tweetnacl-auth": { 239 | "version": "0.3.1", 240 | "resolved": "https://registry.npmjs.org/tweetnacl-auth/-/tweetnacl-auth-0.3.1.tgz", 241 | "integrity": "sha1-t1vC3xVkm7hOi5qjwGacbEvODSU=", 242 | "requires": { 243 | "tweetnacl": "0.x.x" 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /secrethandshake/package-lock.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /secrethandshake/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "secret-handshake": "^1.1.20" 4 | }, 5 | "devDependencies": { 6 | "chloride": "^2.4.1", 7 | "pull-stream": "^3.6.14", 8 | "stream-to-pull-stream": "^1.7.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /secrethandshake/package.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | 3 | SPDX-License-Identifier: Unlicense -------------------------------------------------------------------------------- /secrethandshake/server_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | //go:build interop_nodejs 6 | // +build interop_nodejs 7 | 8 | package secrethandshake 9 | 10 | import ( 11 | "encoding/base64" 12 | "testing" 13 | 14 | "go.mindeco.de/logging/logtest" 15 | "go.mindeco.de/proc" 16 | ) 17 | 18 | func TestServer(t *testing.T) { 19 | var err error 20 | 21 | var kp EdKeyPair 22 | kp.Public, err = base64.StdEncoding.DecodeString("dz9tpZBrNTCCKiA+mIn/x1M6IEO9k05DbkgpqsGbTpE=") 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | kp.Secret, err = base64.StdEncoding.DecodeString("kB4WbT+KvUYwpCxc0FC5+FYKFbUFMoIR+f7VhDVWvU13P22lkGs1MIIqID6Yif/HUzogQ72TTkNuSCmqwZtOkQ==") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | appKey, err := base64.StdEncoding.DecodeString("IhrX11txvFiVzm+NurzHLCqUUe3xZXkPfODnp7WlMpk=") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | serverState, err := NewServerState(appKey, kp) 38 | if err != nil { 39 | t.Fatal("error making server state:", err) 40 | } 41 | 42 | client, err := proc.StartStdioProcess("node", logtest.Logger("client_test.js", t), "client_test.js") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | if err := Server(serverState, client); err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /secrethandshake/server_test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | var shs = require('secret-handshake') 6 | var fs = require('fs') 7 | var pull = require('pull-stream') 8 | var toPull = require('stream-to-pull-stream') 9 | 10 | function readKeyF (fname) { 11 | var tmpobj = JSON.parse(fs.readFileSync(fname).toString()) 12 | return { 13 | publicKey: Buffer.from(tmpobj.publicKey, 'base64'), 14 | secretKey: Buffer.from(tmpobj.secretKey, 'base64') 15 | } 16 | } 17 | 18 | var appKey = Buffer.from('IhrX11txvFiVzm+NurzHLCqUUe3xZXkPfODnp7WlMpk=', 'base64') 19 | 20 | var createBob = shs.createServer(readKeyF('key.bob.json'), function (pub, cb) { 21 | // decide whether to allow access to pub. 22 | cb(null, true) 23 | }, appKey) 24 | 25 | pull( 26 | toPull.source(process.stdin), 27 | createBob(function (err, stream) { 28 | if (err) throw err 29 | // ... 30 | }), 31 | toPull.sink(process.stdout) 32 | ) 33 | -------------------------------------------------------------------------------- /secrethandshake/state.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | /* Package secrethandshake is a Go implementation of Dominic Tarr's secret-handshake: https://github.com/auditdrivencrypto/secret-handshake 6 | 7 | Two instances of go-shs can secretly shake hands over a connection. 8 | 9 | The implementation is compatible with the JS implementation. 10 | Run `npm ci && go test -tags interop_nodejs`. 11 | */ 12 | package secrethandshake 13 | 14 | import ( 15 | "bytes" 16 | "crypto/rand" 17 | "crypto/sha256" 18 | 19 | "github.com/ssbc/go-secretstream/internal/lo25519" 20 | "github.com/ssbc/go-secretstream/secrethandshake/internal/extra25519" 21 | "golang.org/x/crypto/curve25519" 22 | "golang.org/x/crypto/ed25519" 23 | "golang.org/x/crypto/nacl/auth" 24 | "golang.org/x/crypto/nacl/box" 25 | ) 26 | 27 | // State is the state each peer holds during the handshake 28 | type State struct { 29 | appKey [32]byte 30 | 31 | secHash []byte 32 | localAppMac [32]byte 33 | remoteAppMac []byte 34 | 35 | localExchange CurveKeyPair 36 | local EdKeyPair 37 | remoteExchange CurveKeyPair 38 | remotePublic ed25519.PublicKey // long-term 39 | 40 | secret, secret2, secret3 [32]byte 41 | 42 | hello []byte 43 | 44 | aBob, bAlice [32]byte // better name? helloAlice, helloBob? 45 | } 46 | 47 | // EdKeyPair is a keypair for use with github.com/agl/ed25519 48 | type EdKeyPair struct { 49 | Public ed25519.PublicKey 50 | Secret ed25519.PrivateKey 51 | } 52 | 53 | func NewKeyPair(public, secret []byte) (*EdKeyPair, error) { 54 | var kp EdKeyPair 55 | if n := len(secret); n != ed25519.PrivateKeySize { 56 | return nil, ErrKeySize{tipe: "private", n: n} 57 | } 58 | kp.Secret = secret 59 | 60 | if n := len(public); n != ed25519.PublicKeySize { 61 | return nil, ErrKeySize{tipe: "public", n: n} 62 | } 63 | 64 | if lo25519.IsEdLowOrder(public) { 65 | return nil, ErrInvalidKeyPair 66 | } 67 | kp.Public = public 68 | 69 | return &kp, nil 70 | } 71 | 72 | // CurveKeyPair is a keypair for use with github.com/agl/ed25519 73 | type CurveKeyPair struct { 74 | Public [32]byte 75 | Secret [32]byte 76 | } 77 | 78 | // NewClientState initializes the state for the client side 79 | func NewClientState(appKey []byte, local EdKeyPair, remotePublic ed25519.PublicKey) (*State, error) { 80 | state, err := newState(appKey, local) 81 | if err != nil { 82 | return state, err 83 | } 84 | 85 | state.remotePublic = remotePublic 86 | if l := len(state.remotePublic); l != ed25519.PublicKeySize { 87 | return nil, ErrKeySize{tipe: "remote/public", n: l} 88 | } 89 | 90 | return state, err 91 | } 92 | 93 | // NewServerState initializes the state for the server side 94 | func NewServerState(appKey []byte, local EdKeyPair) (*State, error) { 95 | return newState(appKey, local) 96 | } 97 | 98 | // newState initializes the state needed by both client and server 99 | func newState(appKey []byte, local EdKeyPair) (*State, error) { 100 | pubKey, secKey, err := box.GenerateKey(rand.Reader) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | s := State{ 106 | remotePublic: make([]byte, ed25519.PublicKeySize), 107 | } 108 | copy(s.appKey[:], appKey) 109 | copy(s.localExchange.Public[:], pubKey[:]) 110 | copy(s.localExchange.Secret[:], secKey[:]) 111 | s.local = local 112 | 113 | if l := len(s.local.Public); l != ed25519.PublicKeySize { 114 | return nil, ErrKeySize{tipe: "eph/public", n: l} 115 | } 116 | 117 | if l := len(s.local.Secret); l != ed25519.PrivateKeySize { 118 | return nil, ErrKeySize{tipe: "eph/private", n: l} 119 | } 120 | 121 | return &s, nil 122 | } 123 | 124 | // createChallenge returns a buffer with a challenge 125 | func (s *State) createChallenge() []byte { 126 | mac := auth.Sum(s.localExchange.Public[:], &s.appKey) 127 | copy(s.localAppMac[:], mac[:]) 128 | 129 | return append(s.localAppMac[:], s.localExchange.Public[:]...) 130 | } 131 | 132 | // verifyChallenge returns whether the passed buffer is valid 133 | func (s *State) verifyChallenge(ch []byte) bool { 134 | mac := ch[:32] 135 | remoteEphPubKey := ch[32:] 136 | 137 | ok := auth.Verify(mac, remoteEphPubKey, &s.appKey) 138 | 139 | copy(s.remoteExchange.Public[:], remoteEphPubKey) 140 | s.remoteAppMac = mac 141 | 142 | var sec [32]byte 143 | curve25519.ScalarMult(&sec, &s.localExchange.Secret, &s.remoteExchange.Public) 144 | copy(s.secret[:], sec[:]) 145 | 146 | secHasher := sha256.New() 147 | secHasher.Write(s.secret[:]) 148 | s.secHash = secHasher.Sum(nil) 149 | 150 | return ok 151 | } 152 | 153 | // createClientAuth returns a buffer containing a clientAuth message 154 | func (s *State) createClientAuth() []byte { 155 | var curveRemotePubKey [32]byte 156 | if !extra25519.PublicKeyToCurve25519(&curveRemotePubKey, s.remotePublic) { 157 | panic("secrethandshake: could not convert remote to curve key") 158 | } 159 | var aBob [32]byte 160 | curve25519.ScalarMult(&aBob, &s.localExchange.Secret, &curveRemotePubKey) 161 | copy(s.aBob[:], aBob[:]) 162 | 163 | secHasher := sha256.New() 164 | secHasher.Write(s.appKey[:]) 165 | secHasher.Write(s.secret[:]) 166 | secHasher.Write(s.aBob[:]) 167 | copy(s.secret2[:], secHasher.Sum(nil)) 168 | 169 | var sigMsg bytes.Buffer 170 | sigMsg.Write(s.appKey[:]) 171 | sigMsg.Write(s.remotePublic[:]) 172 | sigMsg.Write(s.secHash) 173 | 174 | sig := ed25519.Sign(s.local.Secret, sigMsg.Bytes()) 175 | 176 | var helloBuf bytes.Buffer 177 | helloBuf.Write(sig[:]) 178 | helloBuf.Write(s.local.Public[:]) 179 | s.hello = helloBuf.Bytes() 180 | 181 | out := make([]byte, 0, len(s.hello)-box.Overhead) 182 | var n [24]byte 183 | out = box.SealAfterPrecomputation(out, s.hello, &n, &s.secret2) 184 | return out 185 | } 186 | 187 | var nullHello [ed25519.SignatureSize + ed25519.PublicKeySize]byte 188 | 189 | // verifyClientAuth returns whether a buffer contains a valid clientAuth message 190 | func (s *State) verifyClientAuth(data []byte) bool { 191 | var cvSec, aBob [32]byte 192 | extra25519.PrivateKeyToCurve25519(&cvSec, s.local.Secret) 193 | curve25519.ScalarMult(&aBob, &cvSec, &s.remoteExchange.Public) 194 | copy(s.aBob[:], aBob[:]) 195 | 196 | secHasher := sha256.New() 197 | secHasher.Write(s.appKey[:]) 198 | secHasher.Write(s.secret[:]) 199 | secHasher.Write(s.aBob[:]) 200 | copy(s.secret2[:], secHasher.Sum(nil)) 201 | 202 | s.hello = make([]byte, 0, len(data)-16) 203 | 204 | var nonce [24]byte // always 0? 205 | var openOk bool 206 | s.hello, openOk = box.OpenAfterPrecomputation(s.hello, data, &nonce, &s.secret2) 207 | 208 | var sig = make([]byte, ed25519.SignatureSize) 209 | var public = make([]byte, ed25519.PublicKeySize) 210 | /* TODO: is this const time!?! 211 | 212 | this is definetly not: 213 | if !openOK { 214 | s.hello = nullHello 215 | } 216 | copy(sig, ...) 217 | copy(pub, ...) 218 | */ 219 | if openOk { 220 | copy(sig, s.hello[:ed25519.SignatureSize]) 221 | copy(public[:], s.hello[ed25519.SignatureSize:]) 222 | 223 | } else { 224 | copy(sig, nullHello[:ed25519.SignatureSize]) 225 | copy(public[:], nullHello[ed25519.SignatureSize:]) 226 | } 227 | 228 | if lo25519.IsEdLowOrder(sig[:32]) { 229 | openOk = false 230 | } 231 | 232 | var sigMsg bytes.Buffer 233 | sigMsg.Write(s.appKey[:]) 234 | sigMsg.Write(s.local.Public[:]) 235 | sigMsg.Write(s.secHash) 236 | verifyOk := ed25519.Verify(public, sigMsg.Bytes(), sig) 237 | 238 | copy(s.remotePublic, public) 239 | return openOk && verifyOk 240 | } 241 | 242 | // createServerAccept returns a buffer containing a serverAccept message 243 | func (s *State) createServerAccept() []byte { 244 | var curveRemotePubKey [32]byte 245 | if !extra25519.PublicKeyToCurve25519(&curveRemotePubKey, s.remotePublic) { 246 | panic("secrethandshake: could not convert remote to curve key") 247 | } 248 | var bAlice [32]byte 249 | curve25519.ScalarMult(&bAlice, &s.localExchange.Secret, &curveRemotePubKey) 250 | copy(s.bAlice[:], bAlice[:]) 251 | 252 | secHasher := sha256.New() 253 | secHasher.Write(s.appKey[:]) 254 | secHasher.Write(s.secret[:]) 255 | secHasher.Write(s.aBob[:]) 256 | secHasher.Write(s.bAlice[:]) 257 | copy(s.secret3[:], secHasher.Sum(nil)) 258 | 259 | var sigMsg bytes.Buffer 260 | sigMsg.Write(s.appKey[:]) 261 | sigMsg.Write(s.hello[:]) 262 | sigMsg.Write(s.secHash) 263 | 264 | okay := ed25519.Sign(s.local.Secret, sigMsg.Bytes()) 265 | 266 | var out = make([]byte, 0, len(okay)+16) 267 | var nonce [24]byte // always 0? 268 | return box.SealAfterPrecomputation(out, okay[:], &nonce, &s.secret3) 269 | } 270 | 271 | // verifyServerAccept returns whether the passed buffer contains a valid serverAccept message 272 | func (s *State) verifyServerAccept(boxedOkay []byte) bool { 273 | var curveLocalSec [32]byte 274 | extra25519.PrivateKeyToCurve25519(&curveLocalSec, s.local.Secret) 275 | var bAlice [32]byte 276 | curve25519.ScalarMult(&bAlice, &curveLocalSec, &s.remoteExchange.Public) 277 | copy(s.bAlice[:], bAlice[:]) 278 | 279 | secHasher := sha256.New() 280 | secHasher.Write(s.appKey[:]) 281 | secHasher.Write(s.secret[:]) 282 | secHasher.Write(s.aBob[:]) 283 | secHasher.Write(s.bAlice[:]) 284 | copy(s.secret3[:], secHasher.Sum(nil)) 285 | 286 | var nonce [24]byte // always 0? 287 | sig := make([]byte, 0, len(boxedOkay)-16) 288 | sig, openOk := box.OpenAfterPrecomputation(nil, boxedOkay, &nonce, &s.secret3) 289 | 290 | var sigMsg bytes.Buffer 291 | sigMsg.Write(s.appKey[:]) 292 | sigMsg.Write(s.hello[:]) 293 | sigMsg.Write(s.secHash) 294 | 295 | verifyOk := ed25519.Verify(s.remotePublic, sigMsg.Bytes(), sig) 296 | return verifyOk && openOk 297 | } 298 | 299 | // cleanSecrets overwrites all intermediate secrets and copies the final secret to s.secret 300 | func (s *State) cleanSecrets() { 301 | var zeros [64]byte 302 | 303 | copy(s.secHash, zeros[:]) 304 | copy(s.secret[:], zeros[:]) // redundant 305 | copy(s.aBob[:], zeros[:]) 306 | copy(s.bAlice[:], zeros[:]) 307 | 308 | h := sha256.New() 309 | h.Write(s.secret3[:]) 310 | copy(s.secret[:], h.Sum(nil)) 311 | copy(s.secret2[:], zeros[:]) 312 | copy(s.secret3[:], zeros[:]) 313 | copy(s.localExchange.Secret[:], zeros[:]) 314 | } 315 | 316 | // Remote returns the public key of the remote party 317 | func (s *State) Remote() []byte { 318 | return s.remotePublic[:] 319 | } 320 | 321 | // GetBoxstreamEncKeys returns the encryption key and nonce suitable for boxstream 322 | func (s *State) GetBoxstreamEncKeys() ([32]byte, [24]byte) { 323 | // TODO: error before cleanSecrets() has been called? 324 | 325 | var enKey [32]byte 326 | h := sha256.New() 327 | h.Write(s.secret[:]) 328 | h.Write(s.remotePublic[:]) 329 | copy(enKey[:], h.Sum(nil)) 330 | 331 | var nonce [24]byte 332 | copy(nonce[:], s.remoteAppMac) 333 | return enKey, nonce 334 | } 335 | 336 | // GetBoxstreamDecKeys returns the decryption key and nonce suitable for boxstream 337 | func (s *State) GetBoxstreamDecKeys() ([32]byte, [24]byte) { 338 | // TODO: error before cleanSecrets() has been called? 339 | 340 | var deKey [32]byte 341 | h := sha256.New() 342 | h.Write(s.secret[:]) 343 | h.Write(s.local.Public[:]) 344 | copy(deKey[:], h.Sum(nil)) 345 | 346 | var nonce [24]byte 347 | copy(nonce[:], s.localAppMac[:]) 348 | return deKey, nonce 349 | } 350 | -------------------------------------------------------------------------------- /secrethandshake/string.go: -------------------------------------------------------------------------------- 1 | //go:build dev 2 | // +build dev 3 | 4 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 5 | // 6 | // SPDX-License-Identifier: MIT 7 | 8 | package secrethandshake 9 | 10 | // nice for debugging purposes. no production code 11 | import ( 12 | "bytes" 13 | "encoding/hex" 14 | ) 15 | 16 | func (s *State) String() string { 17 | buf := &bytes.Buffer{} 18 | 19 | buf.WriteString("State {") 20 | 21 | appKeyHex := make([]byte, 2*len(s.appKey)) 22 | hex.Encode(appKeyHex, s.appKey[:]) 23 | buf.WriteString("\n\tappKey: ") 24 | buf.Write(appKeyHex) 25 | 26 | secHashHex := make([]byte, 2*len(s.secHash)) 27 | hex.Encode(secHashHex, s.secHash) 28 | buf.WriteString("\n\tsecHash: ") 29 | buf.Write(secHashHex) 30 | 31 | secretHex := make([]byte, 2*len(s.secret)) 32 | hex.Encode(secretHex, s.secret[:]) 33 | buf.WriteString("\n\tsecret: ") 34 | buf.Write(secretHex) 35 | 36 | secret2Hex := make([]byte, 2*len(s.secret2)) 37 | hex.Encode(secret2Hex, s.secret2[:]) 38 | buf.WriteString("\n\tsecret2: ") 39 | buf.Write(secret2Hex) 40 | 41 | secret3Hex := make([]byte, 2*len(s.secret3)) 42 | hex.Encode(secret3Hex, s.secret3[:]) 43 | buf.WriteString("\n\tsecret3: ") 44 | buf.Write(secret3Hex) 45 | 46 | localPublicHex := make([]byte, 2*len(s.local.Public)) 47 | hex.Encode(localPublicHex, s.local.Public[:]) 48 | buf.WriteString("\n\tlocalPublic: ") 49 | buf.Write(localPublicHex) 50 | 51 | localEphPublicHex := make([]byte, 2*len(s.localExchange.Public)) 52 | hex.Encode(localEphPublicHex, s.localExchange.Public[:]) 53 | buf.WriteString("\n\tlocalEphPublic: ") 54 | buf.Write(localEphPublicHex) 55 | 56 | remotePublicHex := make([]byte, 2*len(s.remotePublic)) 57 | hex.Encode(remotePublicHex, s.remotePublic[:]) 58 | buf.WriteString("\n\tremotePublic: ") 59 | buf.Write(remotePublicHex) 60 | 61 | remoteEphPublicHex := make([]byte, 2*len(s.remoteExchange.Public)) 62 | hex.Encode(remoteEphPublicHex, s.remoteExchange.Public[:]) 63 | buf.WriteString("\n\tremoteEphPublic: ") 64 | buf.Write(remoteEphPublicHex) 65 | 66 | bAliceHex := make([]byte, 2*len(s.bAlice)) 67 | hex.Encode(bAliceHex, s.bAlice[:]) 68 | buf.WriteString("\n\tbAlice: ") 69 | buf.Write(bAliceHex) 70 | 71 | aBobHex := make([]byte, 2*len(s.aBob)) 72 | hex.Encode(aBobHex, s.aBob[:]) 73 | buf.WriteString("\n\taBob: ") 74 | buf.Write(aBobHex) 75 | 76 | remoteHelloHex := make([]byte, 2*len(s.hello)) 77 | hex.Encode(remoteHelloHex, s.hello[:]) 78 | buf.WriteString("\n\tremoteHello: ") 79 | buf.Write(remoteHelloHex) 80 | 81 | buf.WriteString("\n}") 82 | return buf.String() 83 | } 84 | -------------------------------------------------------------------------------- /secrethandshake/tests/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | # 3 | # SPDX-License-Identifier: MIT 4 | 5 | server: server.go 6 | go build server.go 7 | 8 | client: client.go 9 | go build client.go 10 | 11 | test: server client 12 | test -d ../shs1-testsuite/node_modules || \ 13 | ( git submodule update --init && \ 14 | cd ../shs1-testsuite && \ 15 | npm ci && \ 16 | cd - ) 17 | node ../shs1-testsuite/test-server.js ./server 18 | node ../shs1-testsuite/test-client.js ./client -------------------------------------------------------------------------------- /secrethandshake/tests/client.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package main 6 | 7 | import ( 8 | "encoding/hex" 9 | "io" 10 | "os" 11 | 12 | "github.com/ssbc/go-secretstream/secrethandshake" 13 | "go.mindeco.de/logging" 14 | ) 15 | 16 | var check = logging.CheckFatal 17 | 18 | type rw struct { 19 | io.Reader 20 | io.Writer 21 | } 22 | 23 | func main() { 24 | //logging.SetupLogging(nil) 25 | //log := logging.Logger("goshs-test-client") 26 | 27 | appKey, err := hex.DecodeString(os.Args[1]) 28 | check(err) 29 | 30 | remotePublic, err := hex.DecodeString(os.Args[2]) 31 | check(err) 32 | 33 | keyPair, err := secrethandshake.GenEdKeyPair(nil) 34 | check(err) 35 | 36 | s, err := secrethandshake.NewClientState(appKey, *keyPair, remotePublic) 37 | check(err) 38 | 39 | err = secrethandshake.Client(s, rw{os.Stdin, os.Stdout}) 40 | check(err) 41 | 42 | encKey, encNonce := s.GetBoxstreamEncKeys() 43 | os.Stdout.Write(encKey[:]) 44 | os.Stdout.Write(encNonce[:]) 45 | 46 | decKey, decNonnce := s.GetBoxstreamDecKeys() 47 | os.Stdout.Write(decKey[:]) 48 | os.Stdout.Write(decNonnce[:]) 49 | } 50 | -------------------------------------------------------------------------------- /secrethandshake/tests/server.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | //go:build none 6 | // +build none 7 | 8 | package main 9 | 10 | import ( 11 | "encoding/hex" 12 | "io" 13 | "os" 14 | 15 | "github.com/ssbc/go-secretstream/secrethandshake" 16 | "go.mindeco.de/logging" 17 | ) 18 | 19 | var check = logging.CheckFatal 20 | 21 | type rw struct { 22 | io.Reader 23 | io.Writer 24 | } 25 | 26 | func main() { 27 | //logging.SetupLogging(nil) 28 | //log := logging.Logger("goshs-test-server") 29 | 30 | appKey, err := hex.DecodeString(os.Args[1]) 31 | check(err) 32 | 33 | var keyPair secrethandshake.EdKeyPair 34 | keyPair.Secret, err = hex.DecodeString(os.Args[2]) 35 | check(err) 36 | 37 | keyPair.Public, err = hex.DecodeString(os.Args[3]) 38 | check(err) 39 | 40 | s, err := secrethandshake.NewServerState(appKey, keyPair) 41 | check(err) 42 | 43 | err = secrethandshake.Server(s, rw{os.Stdin, os.Stdout}) 44 | check(err) 45 | 46 | encKey, encNonce := s.GetBoxstreamEncKeys() 47 | os.Stdout.Write(encKey[:]) 48 | os.Stdout.Write(encNonce[:]) 49 | 50 | decKey, decNonnce := s.GetBoxstreamDecKeys() 51 | os.Stdout.Write(decKey[:]) 52 | os.Stdout.Write(decNonnce[:]) 53 | } 54 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 The Secretstream Authors 2 | // 3 | // SPDX-License-Identifier: MIT 4 | 5 | package secretstream 6 | 7 | import ( 8 | "fmt" 9 | "net" 10 | "time" 11 | 12 | "github.com/ssbc/go-secretstream/boxstream" 13 | "github.com/ssbc/go-secretstream/secrethandshake" 14 | 15 | "github.com/ssbc/go-netwrap" 16 | ) 17 | 18 | // Server can create net.Listeners 19 | type Server struct { 20 | keyPair secrethandshake.EdKeyPair 21 | appKey []byte 22 | } 23 | 24 | // NewServer returns a Server which uses the passed keyPair and appKey 25 | func NewServer(keyPair secrethandshake.EdKeyPair, appKey []byte) (*Server, error) { 26 | return &Server{keyPair: keyPair, appKey: appKey}, nil 27 | } 28 | 29 | // ListenerWrapper returns a listener wrapper. 30 | func (s *Server) ListenerWrapper() netwrap.ListenerWrapper { 31 | return netwrap.NewListenerWrapper(s.Addr(), s.ConnWrapper()) 32 | } 33 | 34 | // ConnWrapper returns a connection wrapper. 35 | func (s *Server) ConnWrapper() netwrap.ConnWrapper { 36 | return func(conn net.Conn) (net.Conn, error) { 37 | state, err := secrethandshake.NewServerState(s.appKey, s.keyPair) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | errc := make(chan error) 47 | go func() { 48 | errc <- secrethandshake.Server(state, conn) 49 | close(errc) 50 | }() 51 | 52 | select { 53 | case err := <-errc: 54 | if err != nil { 55 | return nil, err 56 | } 57 | case <-time.After(2 * time.Minute): 58 | return nil, fmt.Errorf("secretstream: handshake timeout") 59 | } 60 | 61 | enKey, enNonce := state.GetBoxstreamEncKeys() 62 | deKey, deNonce := state.GetBoxstreamDecKeys() 63 | 64 | remote := state.Remote() 65 | boxed := &Conn{ 66 | boxer: boxstream.NewBoxer(conn, &enNonce, &enKey), 67 | unboxer: boxstream.NewUnboxer(conn, &deNonce, &deKey), 68 | conn: conn, 69 | local: s.keyPair.Public[:], 70 | remote: remote[:], 71 | } 72 | 73 | return boxed, nil 74 | } 75 | } 76 | 77 | // Addr returns the shs-bs address of the server. 78 | func (s *Server) Addr() net.Addr { 79 | return Addr{s.keyPair.Public[:]} 80 | } 81 | --------------------------------------------------------------------------------