├── version ├── go.mod ├── .golangci.yml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── mattermost-webhook.yml │ ├── main.yml │ ├── golangci-lint.yml │ ├── mattermost-channel-posts.yml │ └── codeql-analysis.yml ├── SECURITY.md ├── CODE_OF_CONDUCT.md ├── go.sum ├── .gitignore ├── sodium_test.go ├── README.md ├── LICENSE ├── stream_test.go ├── kx ├── kx.go └── kx_test.go ├── sodium.go └── stream.go /version: -------------------------------------------------------------------------------- 1 | 0.1 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/openziti/secretstream 2 | 3 | go 1.24.0 4 | 5 | require golang.org/x/crypto v0.43.0 6 | 7 | require golang.org/x/sys v0.37.0 // indirect 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | settings: 4 | staticcheck: 5 | checks: [ "all", "-ST1000", "-ST1003", "-ST1006", "-ST1020", "-ST1021", "-ST1012" ] 6 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # see: 2 | # https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/creating-a-repository-on-github/about-code-owners 3 | * @openziti/sig-core 4 | 5 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Please refer to the [openziti-security repository](https://github.com/openziti/openziti-security) for details of the security policies and processes for this repository. -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | All open source projects managed by OpenZiti share a common [code of conduct](https://docs.openziti.io/policies/CODE_OF_CONDUCT.html) which all contributors are expected to follow. Please be sure you read, understand and adhere to the guidelines expressed therein. 2 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 2 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 3 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 4 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea/ 18 | 19 | go.work 20 | go.work.sum 21 | -------------------------------------------------------------------------------- /sodium_test.go: -------------------------------------------------------------------------------- 1 | // +build compat_test 2 | 3 | package secretstream 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestToSodium(t *testing.T) { 10 | common_test(t, NewEncryptor, NewSodiumRecvStream) 11 | } 12 | 13 | func TestFromSodium(t *testing.T) { 14 | common_test(t, NewSodiumSendStream, NewDecryptor) 15 | } 16 | 17 | func TestSodium2Sodium(t *testing.T) { 18 | common_test(t, NewSodiumSendStream, NewSodiumRecvStream) 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | groups: 9 | non-major: 10 | applies-to: version-updates 11 | update-types: 12 | - "minor" 13 | - "patch" 14 | 15 | - package-ecosystem: github-actions 16 | directory: "/" 17 | schedule: 18 | interval: weekly 19 | open-pull-requests-limit: 10 20 | groups: 21 | all: 22 | applies-to: version-updates 23 | update-types: 24 | - "major" 25 | - "minor" 26 | - "patch" 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/mattermost-webhook.yml: -------------------------------------------------------------------------------- 1 | name: mattermost-ziti-webhook 2 | on: 3 | create: 4 | delete: 5 | issues: 6 | issue_comment: 7 | pull_request_review: 8 | pull_request_review_comment: 9 | pull_request: 10 | push: 11 | fork: 12 | release: 13 | 14 | jobs: 15 | mattermost-ziti-webhook: 16 | runs-on: ubuntu-latest 17 | name: POST Webhook 18 | if: github.actor != 'dependabot[bot]' 19 | env: 20 | ZITI_LOG: 99 21 | ZITI_NODEJS_LOG: 99 22 | steps: 23 | - uses: openziti/ziti-webhook-action@main 24 | with: 25 | ziti-id: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} 26 | webhook-url: ${{ secrets.ZITI_MATTERMOST_WEBHOOK_URL }} 27 | webhook-secret: ${{ secrets.ZITI_MATTERMOSTI_WEBHOOK_SECRET }} 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # secretstream 2 | Implementation of [libsodium](https://github.com/jedisct1/libsodium)'s [secretstream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream) in Go 3 | 4 | The main goal of this project is allow using `secretstream` between programs using libsodium and 5 | programs written in Go without resorting to wrapping libsodium in Go. golang.org/x/crypto has all necessary 6 | algorithms to make that happen. 7 | 8 | ## Testing against libsodium 9 | It is important that this implementation is compatible with libsodium. Tests tagged with `compat_test` use libsodium to test compatibility. 10 | 11 | make sure you have libsodium installed and ready to be used 12 | ```bash 13 | $ sudo apt install libsodium libsodium-dev 14 | ``` 15 | _other platforms something similar_ 16 | 17 | You're ready to run tests! 18 | ```bash 19 | $ go test --tags=compat_test ./... 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 NetFoundry, Inc 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - release-* 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Git Checkout 16 | uses: actions/checkout@v5 17 | with: 18 | persist-credentials: false 19 | 20 | - name: Install Go 21 | uses: actions/setup-go@v6 22 | with: 23 | go-version-file: ./go.mod 24 | 25 | - name: install libsodium (for compat tests) 26 | run: sudo apt install libsodium-dev 27 | 28 | - name: Install Ziti CI 29 | uses: openziti/ziti-ci@v1 30 | 31 | - name: Build and Test 32 | run: go test --tags=compat_test ./... 33 | 34 | - name: Release 35 | env: 36 | gh_ci_key: ${{ secrets.GH_CI_KEY }} 37 | ziti_ci_gpg_key: ${{ secrets.ZITI_CI_GPG_KEY }} 38 | ziti_ci_gpg_key_id: ${{ secrets.ZITI_CI_GPG_KEY_ID }} 39 | if: github.ref_name == 'main' || startsWith(github.ref_name, 'release-') 40 | run: | 41 | $(go env GOPATH)/bin/ziti-ci configure-git 42 | $(go env GOPATH)/bin/ziti-ci tag -v -f version 43 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | pull_request: 4 | permissions: 5 | contents: read 6 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 7 | # pull-requests: read 8 | 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v5 15 | 16 | - uses: actions/setup-go@v6 17 | with: 18 | go-version-file: ./go.mod 19 | 20 | - name: golangci-lint 21 | uses: golangci/golangci-lint-action@v8 22 | with: 23 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 24 | version: latest 25 | 26 | # Optional: working directory, useful for monorepos 27 | # working-directory: somedir 28 | 29 | # Optional: golangci-lint command line arguments. 30 | # args: --issues-exit-code=0 31 | 32 | # Optional: show only new issues if it's a pull request. The default value is `false`. 33 | # only-new-issues: true 34 | 35 | # Optional: if set to true then the all caching functionality will be complete disabled, 36 | # takes precedence over all other caching options. 37 | # skip-cache: true 38 | 39 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 40 | # skip-pkg-cache: true 41 | 42 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 43 | # skip-build-cache: true 44 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | package secretstream 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "testing" 7 | ) 8 | 9 | func TestEncodeDecode(t *testing.T) { 10 | common_test(t, NewEncryptor, NewDecryptor) 11 | } 12 | 13 | func common_test(t *testing.T, 14 | makeEnc func([]byte) (Encryptor, []byte, error), 15 | makeDec func(k, h []byte) (Decryptor, error)) { 16 | key, err := NewStreamKey() 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | sender, hdr, err := makeEnc(key) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | plain_text_messages := [][]byte{ 27 | []byte("Hello world"), 28 | randomData(100), 29 | randomData(1000), 30 | randomData(10000), 31 | []byte("This is good-bye!"), 32 | } 33 | 34 | var coded_msgs [][]byte 35 | 36 | for i, m := range plain_text_messages { 37 | coded, err := sender.Push(m, byte(i%2)) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | coded_msgs = append(coded_msgs, coded) 42 | } 43 | 44 | var decoded_msgs [][]byte 45 | receiver, err := makeDec(key, hdr) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | for i, m := range coded_msgs { 51 | decoded, tag, err := receiver.Pull(m) 52 | if err != nil { 53 | t.Error("decoding error", err) 54 | } 55 | if tag != byte(i%2) { 56 | t.Errorf("unexpected tag received") 57 | } 58 | decoded_msgs = append(decoded_msgs, decoded) 59 | } 60 | 61 | for i := range plain_text_messages { 62 | if !bytes.Equal(plain_text_messages[i], decoded_msgs[i]) { 63 | t.Error("failed to decode") 64 | } 65 | } 66 | } 67 | 68 | func randomData(c int) []byte { 69 | out := make([]byte, c) 70 | if _, err := rand.Read(out); err != nil { 71 | panic(err) 72 | } 73 | return out 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/mattermost-channel-posts.yml: -------------------------------------------------------------------------------- 1 | name: mattermost-ziti-webhook 2 | on: 3 | issues: 4 | issue_comment: 5 | pull_request_review: 6 | types: [ submitted ] 7 | pull_request_review_comment: 8 | pull_request: 9 | types: [ opened, reopened, ready_for_review, closed ] 10 | fork: 11 | push: 12 | tags: 13 | - '*' 14 | release: 15 | types: [ released ] 16 | workflow_dispatch: 17 | watch: 18 | types: [ started ] 19 | 20 | jobs: 21 | send-notifications: 22 | runs-on: ubuntu-latest 23 | name: POST Webhook 24 | if: github.actor != 'dependabot[bot]' 25 | steps: 26 | - uses: openziti/ziti-mattermost-action-py@main 27 | if: | 28 | github.repository_owner == 'openziti' 29 | && ((github.event_name != 'pull_request_review') 30 | || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) 31 | with: 32 | zitiId: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} 33 | webhookUrl: ${{ secrets.ZHOOK_URL }} 34 | eventJson: ${{ toJson(github.event) }} 35 | senderUsername: "GitHubZ" 36 | destChannel: "dev-notifications" 37 | 38 | - uses: openziti/ziti-mattermost-action-py@main 39 | if: | 40 | github.repository_owner == 'openziti' 41 | && ((github.event_name != 'pull_request_review') 42 | || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved')) 43 | with: 44 | zitiId: ${{ secrets.ZITI_MATTERMOST_IDENTITY }} 45 | webhookUrl: ${{ secrets.ZHOOK_URL }} 46 | eventJson: ${{ toJson(github.event) }} 47 | senderUsername: "GitHubZ" 48 | destChannel: "github-sig-core" 49 | 50 | -------------------------------------------------------------------------------- /kx/kx.go: -------------------------------------------------------------------------------- 1 | package kx 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "golang.org/x/crypto/blake2b" 7 | "golang.org/x/crypto/curve25519" 8 | ) 9 | 10 | const SeedBytes = 32 11 | const SecretKeyBytes = 32 12 | const PublicKeyBytes = 32 13 | const SessionKeyBytes = 32 14 | 15 | // const scalarMultBytes = 32 16 | 17 | var cryptoError = errors.New("crypto error") 18 | 19 | type KeyPair struct { 20 | pk [SessionKeyBytes]byte 21 | sk [PublicKeyBytes]byte 22 | } 23 | 24 | func NewKeyPair() (*KeyPair, error) { 25 | var err error 26 | seed := make([]byte, SeedBytes) 27 | _, err = rand.Read(seed) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return newKeyPairFromSeed(seed) 33 | } 34 | 35 | func newKeyPairFromSeed(seed []byte) (*KeyPair, error) { 36 | var err error 37 | kp := new(KeyPair) 38 | 39 | hash, _ := blake2b.New(SecretKeyBytes, nil) 40 | hash.Write(seed) 41 | sk := hash.Sum(nil) 42 | if len(sk) != SecretKeyBytes { 43 | return nil, cryptoError 44 | } 45 | copy(kp.sk[:], sk) 46 | 47 | pk, err := curve25519.X25519(kp.sk[:], curve25519.Basepoint) 48 | if err != nil { 49 | return nil, err 50 | } 51 | if len(pk) != PublicKeyBytes { 52 | return nil, cryptoError 53 | } 54 | copy(kp.pk[:], pk) 55 | 56 | return kp, nil 57 | } 58 | 59 | func (pair *KeyPair) Public() []byte { 60 | return pair.pk[:] 61 | } 62 | 63 | func (pair *KeyPair) ClientSessionKeys(server_pk []byte) (rx []byte, tx []byte, err error) { 64 | q, err := curve25519.X25519(pair.sk[:], server_pk) 65 | if err != nil { 66 | return nil, nil, err 67 | } 68 | 69 | h, err := blake2b.New(2*SessionKeyBytes, nil) 70 | if err != nil { 71 | return nil, nil, err 72 | } 73 | 74 | for _, b := range [][]byte{q, pair.Public(), server_pk} { 75 | if _, err = h.Write(b); err != nil { 76 | return nil, nil, err 77 | } 78 | } 79 | 80 | keys := h.Sum(nil) 81 | 82 | return keys[:SessionKeyBytes], keys[SecretKeyBytes:], nil 83 | 84 | } 85 | 86 | func (pair *KeyPair) ServerSessionKeys(client_pk []byte) (rx []byte, tx []byte, err error) { 87 | 88 | q, err := curve25519.X25519(pair.sk[:], client_pk) 89 | if err != nil { 90 | return nil, nil, err 91 | } 92 | 93 | h, err := blake2b.New(2*SessionKeyBytes, nil) 94 | if err != nil { 95 | return nil, nil, err 96 | } 97 | 98 | for _, b := range [][]byte{q, client_pk, pair.Public()} { 99 | if _, err = h.Write(b); err != nil { 100 | return nil, nil, err 101 | } 102 | } 103 | 104 | keys := h.Sum(nil) 105 | 106 | return keys[SessionKeyBytes:], keys[:SecretKeyBytes], nil 107 | } 108 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '40 13 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v5 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v4 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v4 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v4 72 | -------------------------------------------------------------------------------- /sodium.go: -------------------------------------------------------------------------------- 1 | // +build compat_test 2 | 3 | package secretstream 4 | 5 | // #cgo LDFLAGS: -lsodium 6 | // #include 7 | // #include 8 | import "C" 9 | import ( 10 | "errors" 11 | ) 12 | 13 | type sodiumStream struct { 14 | state C.crypto_secretstream_xchacha20poly1305_state 15 | } 16 | 17 | func NewSodiumSendStream(key []byte) (Encryptor, []byte, error) { 18 | res := &sodiumStream{} 19 | 20 | ckey := C.CBytes(key) 21 | defer C.free(ckey) 22 | 23 | header := C.malloc(C.crypto_secretstream_xchacha20poly1305_HEADERBYTES) 24 | defer C.free(header) 25 | 26 | rc := C.crypto_secretstream_xchacha20poly1305_init_push(&res.state, (*C.uchar)(header), (*C.uchar)(ckey)) 27 | if rc != 0 { 28 | return nil, nil, cryptoFailure 29 | } 30 | // fmt.Printf("sodium: %+v\n", res.state) 31 | 32 | return res, C.GoBytes(header, C.crypto_secretstream_xchacha20poly1305_HEADERBYTES), nil 33 | } 34 | 35 | func NewSodiumRecvStream(key []byte, header []byte) (Decryptor, error) { 36 | res := &sodiumStream{} 37 | 38 | chdr := C.CBytes(header) 39 | defer C.free(chdr) 40 | 41 | ckey := C.CBytes(key) 42 | defer C.free(ckey) 43 | 44 | rc := C.crypto_secretstream_xchacha20poly1305_init_pull(&res.state, (*C.uchar)(chdr), (*C.uchar)(ckey)) 45 | if rc != 0 { 46 | return nil, cryptoFailure 47 | } 48 | // fmt.Printf("receiver = %+v", res.state) 49 | 50 | return res, nil 51 | } 52 | 53 | func (s *sodiumStream) Push(plaintext []byte, tag byte) ([]byte, error) { 54 | pt := C.CBytes(plaintext) 55 | defer C.free(pt) 56 | 57 | cipher_len := len(plaintext) + C.crypto_secretstream_xchacha20poly1305_ABYTES 58 | ct := C.malloc((C.size_t)(cipher_len)) 59 | defer C.free(ct) 60 | 61 | cipher_len_ull := C.ulonglong(cipher_len) 62 | pt_len_ull := C.ulonglong(len(plaintext)) 63 | if C.crypto_secretstream_xchacha20poly1305_push(&s.state, 64 | (*C.uchar)(ct), &cipher_len_ull, 65 | (*C.uchar)(pt), pt_len_ull, 66 | nil, 0, 67 | C.uchar(tag)) != 0 { 68 | return nil, errors.New("sodium error") 69 | } 70 | 71 | return C.GoBytes(ct, C.int(cipher_len)), nil 72 | } 73 | 74 | func (s *sodiumStream) Pull(ciphertext []byte) ([]byte, byte, error) { 75 | ctc := C.CBytes(ciphertext) 76 | defer C.free(ctc) 77 | 78 | mlen := C.ulong(len(ciphertext) - C.crypto_secretstream_xchacha20poly1305_ABYTES) 79 | mlen_ull := C.ulonglong(mlen) 80 | msg := C.malloc((C.size_t)(mlen)) 81 | var tag C.uchar 82 | 83 | if C.crypto_secretstream_xchacha20poly1305_pull(&s.state, 84 | (*C.uchar)(msg), &mlen_ull, &tag, 85 | (*C.uchar)(ctc), C.ulonglong(len(ciphertext)), 86 | nil, 0) != 0 { 87 | return nil, 0, cryptoFailure 88 | } 89 | // fmt.Printf("receiver = %+v", s.state) 90 | 91 | return C.GoBytes(msg, C.int(mlen)), byte(tag), nil 92 | } 93 | -------------------------------------------------------------------------------- /kx/kx_test.go: -------------------------------------------------------------------------------- 1 | package kx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | var seed = makeSeed() 11 | 12 | func makeSeed() []byte { 13 | s := make([]byte, SeedBytes) 14 | for i := range s { 15 | s[i] = byte(i) 16 | } 17 | return s 18 | } 19 | 20 | func seedIncrement(s []byte) []byte { 21 | r := make([]byte, len(s)) 22 | c := uint16(1) 23 | 24 | for i := range s { 25 | c += uint16(s[i]) 26 | r[i] = byte(c) 27 | c >>= 8 28 | } 29 | 30 | return r 31 | } 32 | 33 | func TestNewKeyPair(t *testing.T) { 34 | pk, _ := hex.DecodeString("0e0216223f147143d32615a91189c288c1728cba3cc5f9f621b1026e03d83129") 35 | sk, _ := hex.DecodeString("cb2f5160fc1f7e05a55ef49d340b48da2e5a78099d53393351cd579dd42503d6") 36 | kp := &KeyPair{} 37 | copy(kp.pk[:], pk) 38 | copy(kp.sk[:], sk) 39 | 40 | type args struct { 41 | seed []byte 42 | } 43 | 44 | tests := []struct { 45 | name string 46 | args args 47 | want *KeyPair 48 | wantErr bool 49 | }{ 50 | { 51 | name: "pre-seeded key", 52 | args: args{seed: seed}, 53 | want: kp, 54 | wantErr: false, 55 | }, 56 | } 57 | for _, tt := range tests { 58 | t.Run(tt.name, func(t *testing.T) { 59 | got, err := newKeyPairFromSeed(tt.args.seed) 60 | if (err != nil) != tt.wantErr { 61 | t.Errorf("NewKeyPair() error = %v, wantErr %v", err, tt.wantErr) 62 | return 63 | } 64 | if !reflect.DeepEqual(got, tt.want) { 65 | t.Errorf("NewKeyPair() got = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func TestKeyExchange_Seeded(t *testing.T) { 72 | client_pair, err := newKeyPairFromSeed(seed) 73 | if err != nil { 74 | t.Errorf("failed to get client key pair") 75 | return 76 | } 77 | server_pair, err := newKeyPairFromSeed(seedIncrement(seed)) 78 | 79 | if err != nil { 80 | t.Errorf("failed to get server key pair") 81 | return 82 | } 83 | 84 | clt_rx, _ := hex.DecodeString("749519c68059bce69f7cfcc7b387a3de1a1e8237d110991323bf62870115731a") 85 | clt_tx, _ := hex.DecodeString("62c8f4fa81800abd0577d99918d129b65deb789af8c8351f391feb0cbf238604") 86 | 87 | client_rx, client_tx, err := client_pair.ClientSessionKeys(server_pair.Public()) 88 | if err != nil { 89 | t.Errorf("ClientSessionKeys: error = %v", err) 90 | return 91 | } 92 | 93 | if !bytes.Equal(clt_rx, client_rx) { 94 | t.Errorf("ClientSessionKeys(): RX got = %v, want %v", client_rx, clt_rx) 95 | } 96 | if !bytes.Equal(clt_tx, client_tx) { 97 | t.Errorf("ClientSessionKeys(): TX got = %v, want %v", client_tx, clt_tx) 98 | } 99 | 100 | server_rx, server_tx, err := server_pair.ServerSessionKeys(client_pair.Public()) 101 | if err != nil { 102 | t.Errorf("ServerSessionKeys: error = %v", err) 103 | return 104 | } 105 | 106 | if !bytes.Equal(server_rx, client_tx) || 107 | !bytes.Equal(server_tx, client_rx) { 108 | t.Errorf("ServersSessionKeys(): do not match client's got = %v, want %v", server_rx, clt_tx) 109 | t.Errorf("ServersSessionKeys(): do not match client's got = %v, want %v", server_tx, clt_rx) 110 | return 111 | } 112 | } 113 | 114 | func TestKeyExchange(t *testing.T) { 115 | client_pair, err := NewKeyPair() 116 | if err != nil { 117 | t.Errorf("failed to get client key pair") 118 | return 119 | } 120 | server_pair, err := NewKeyPair() 121 | 122 | if err != nil { 123 | t.Errorf("failed to get server key pair") 124 | return 125 | } 126 | 127 | client_rx, client_tx, err := client_pair.ClientSessionKeys(server_pair.Public()) 128 | if err != nil { 129 | t.Errorf("ClientSessionKeys: error = %v", err) 130 | return 131 | } 132 | 133 | server_rx, server_tx, err := server_pair.ServerSessionKeys(client_pair.Public()) 134 | if err != nil { 135 | t.Errorf("ServerSessionKeys: error = %v", err) 136 | return 137 | } 138 | 139 | if !bytes.Equal(server_rx, client_tx) { 140 | t.Errorf("ServersSessionKeys(): do not match client's got = %v, want %v", server_rx, client_tx) 141 | return 142 | } 143 | 144 | if !bytes.Equal(server_tx, client_rx) { 145 | t.Errorf("ServersSessionKeys(): do not match client's got = %v, want %v", server_tx, client_rx) 146 | return 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | package secretstream 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/subtle" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "golang.org/x/crypto/chacha20" 10 | "golang.org/x/crypto/chacha20poly1305" 11 | // nolint:staticcheck 12 | "golang.org/x/crypto/poly1305" 13 | ) 14 | 15 | // public constants 16 | const ( 17 | TagMessage = 0 18 | TagPush = 0x01 19 | TagRekey = 0x02 20 | TagFinal = TagPush | TagRekey 21 | 22 | StreamKeyBytes = chacha20poly1305.KeySize 23 | StreamHeaderBytes = chacha20poly1305.NonceSizeX 24 | StreamABytes = 16 + 1 25 | ) 26 | 27 | const crypto_core_hchacha20_INPUTBYTES = 16 28 | 29 | /* const crypto_secretstream_xchacha20poly1305_INONCEBYTES = 8 */ 30 | const crypto_secretstream_xchacha20poly1305_COUNTERBYTES = 4 31 | 32 | var pad0 [16]byte 33 | 34 | var invalidKey = errors.New("invalid key") 35 | var invalidInput = errors.New("invalid input") 36 | var cryptoFailure = errors.New("crypto failed") 37 | 38 | type streamState struct { 39 | k [StreamKeyBytes]byte 40 | nonce [chacha20poly1305.NonceSize]byte 41 | pad [8]byte 42 | } 43 | 44 | func (s *streamState) reset() { 45 | for i := range s.nonce { 46 | s.nonce[i] = 0 47 | } 48 | s.nonce[0] = 1 49 | } 50 | 51 | type Encryptor interface { 52 | Push(m []byte, tag byte) ([]byte, error) 53 | } 54 | 55 | type Decryptor interface { 56 | Pull(m []byte) ([]byte, byte, error) 57 | } 58 | 59 | type encryptor struct { 60 | streamState 61 | } 62 | 63 | type decryptor struct { 64 | streamState 65 | } 66 | 67 | func NewStreamKey() ([]byte, error) { 68 | k := make([]byte, chacha20poly1305.KeySize) 69 | _, err := rand.Read(k) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return k, nil 74 | } 75 | 76 | func NewEncryptor(key []byte) (Encryptor, []byte, error) { 77 | if len(key) != StreamKeyBytes { 78 | return nil, nil, invalidKey 79 | } 80 | 81 | header := make([]byte, StreamHeaderBytes) 82 | _, err := rand.Read(header) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | stream := &encryptor{} 88 | 89 | k, err := chacha20.HChaCha20(key[:], header[:16]) 90 | if err != nil { 91 | //fmt.Printf("error: %v", err) 92 | return nil, nil, err 93 | } 94 | copy(stream.k[:], k) 95 | stream.reset() 96 | 97 | for i := range stream.pad { 98 | stream.pad[i] = 0 99 | } 100 | 101 | for i, b := range header[crypto_core_hchacha20_INPUTBYTES:] { 102 | stream.nonce[i+crypto_secretstream_xchacha20poly1305_COUNTERBYTES] = b 103 | } 104 | // fmt.Printf("stream: %+v\n", stream.streamState) 105 | 106 | return stream, header, nil 107 | } 108 | 109 | func (s *encryptor) Push(plain []byte, tag byte) ([]byte, error) { 110 | var err error 111 | 112 | //crypto_onetimeauth_poly1305_state poly1305_state; 113 | var poly *poly1305.MAC 114 | 115 | //unsigned char block[64U]; 116 | var block [64]byte 117 | 118 | //unsigned char slen[8U]; 119 | var slen [8]byte 120 | 121 | //unsigned char *c; 122 | //unsigned char *mac; 123 | // 124 | //if (outlen_p != NULL) { 125 | //*outlen_p = 0U; 126 | //} 127 | 128 | mlen := len(plain) 129 | //if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) { 130 | //sodium_misuse(); 131 | //} 132 | 133 | out := make([]byte, mlen+StreamABytes) 134 | 135 | chacha, err := chacha20.NewUnauthenticatedCipher(s.k[:], s.nonce[:]) 136 | if err != nil { 137 | return nil, err 138 | } 139 | //crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k); 140 | chacha.XORKeyStream(block[:], block[:]) 141 | 142 | //crypto_onetimeauth_poly1305_init(&poly1305_state, block); 143 | var poly_init [32]byte 144 | copy(poly_init[:], block[:]) 145 | poly = poly1305.New(&poly_init) 146 | 147 | // TODO add support for add data 148 | //sodium_memzero(block, sizeof block); 149 | //crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen); 150 | //crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0, 151 | //(0x10 - adlen) & 0xf); 152 | 153 | //memset(block, 0, sizeof block); 154 | //block[0] = tag; 155 | memzero(block[:]) 156 | block[0] = tag 157 | 158 | // 159 | //crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block, state->nonce, 1U, state->k); 160 | //crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block); 161 | //out[0] = block[0]; 162 | chacha.XORKeyStream(block[:], block[:]) 163 | _, _ = poly.Write(block[:]) 164 | out[0] = block[0] 165 | 166 | // 167 | //c = out + (sizeof tag); 168 | c := out[1:] 169 | //crypto_stream_chacha20_ietf_xor_ic(c, m, mlen, state->nonce, 2U, state->k); 170 | //crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen); 171 | //crypto_onetimeauth_poly1305_update (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf); 172 | chacha.XORKeyStream(c, plain) 173 | _, _ = poly.Write(c[:mlen]) 174 | padlen := (0x10 - len(block) + mlen) & 0xf 175 | _, _ = poly.Write(pad0[:padlen]) 176 | 177 | // 178 | //STORE64_LE(slen, (uint64_t) adlen); 179 | //crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen); 180 | binary.LittleEndian.PutUint64(slen[:], uint64(0)) 181 | _, _ = poly.Write(slen[:]) 182 | 183 | //STORE64_LE(slen, (sizeof block) + mlen); 184 | //crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen); 185 | binary.LittleEndian.PutUint64(slen[:], uint64(len(block)+mlen)) 186 | _, _ = poly.Write(slen[:]) 187 | 188 | // 189 | //mac = c + mlen; 190 | //crypto_onetimeauth_poly1305_final(&poly1305_state, mac); 191 | mac := c[mlen:] 192 | copy(mac, poly.Sum(nil)) 193 | //sodium_memzero(&poly1305_state, sizeof poly1305_state); 194 | // 195 | 196 | //XOR_BUF(STATE_INONCE(state), mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES); 197 | //sodium_increment(STATE_COUNTER(state), crypto_secretstream_xchacha20poly1305_COUNTERBYTES); 198 | xor_buf(s.nonce[crypto_secretstream_xchacha20poly1305_COUNTERBYTES:], mac) 199 | buf_inc(s.nonce[:crypto_secretstream_xchacha20poly1305_COUNTERBYTES]) 200 | 201 | // TODO 202 | //if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 || 203 | //sodium_is_zero(STATE_COUNTER(state), 204 | //crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) { 205 | //crypto_secretstream_xchacha20poly1305_rekey(state); 206 | //} 207 | 208 | //if (outlen_p != NULL) { 209 | //*outlen_p = crypto_secretstream_xchacha20poly1305_ABYTES + mlen; 210 | //} 211 | 212 | //return 0; 213 | return out, nil 214 | } 215 | 216 | func NewDecryptor(key, header []byte) (Decryptor, error) { 217 | stream := &decryptor{} 218 | 219 | //crypto_core_hchacha20(state->k, in, k, NULL); 220 | k, err := chacha20.HChaCha20(key, header[:16]) 221 | if err != nil { 222 | fmt.Printf("error: %v", err) 223 | return nil, err 224 | } 225 | copy(stream.k[:], k) 226 | 227 | //_crypto_secretstream_xchacha20poly1305_counter_reset(state); 228 | stream.reset() 229 | 230 | //memcpy(STATE_INONCE(state), in + crypto_core_hchacha20_INPUTBYTES, 231 | // crypto_secretstream_xchacha20poly1305_INONCEBYTES); 232 | copy(stream.nonce[crypto_secretstream_xchacha20poly1305_COUNTERBYTES:], 233 | header[crypto_core_hchacha20_INPUTBYTES:]) 234 | 235 | //memset(state->_pad, 0, sizeof state->_pad); 236 | copy(stream.pad[:], pad0[:]) 237 | 238 | //fmt.Printf("decryptor: %+v\n", stream.streamState) 239 | 240 | return stream, nil 241 | } 242 | 243 | func (s *decryptor) Pull(in []byte) ([]byte, byte, error) { 244 | inlen := len(in) 245 | //crypto_onetimeauth_poly1305_state poly1305_state; 246 | 247 | //unsigned char block[64U]; 248 | var block [64]byte 249 | 250 | //unsigned char slen[8U]; 251 | var slen [8]byte 252 | 253 | //unsigned char mac[crypto_onetimeauth_poly1305_BYTES]; 254 | //const unsigned char *c; 255 | //const unsigned char *stored_mac; 256 | //unsigned long long mlen; 257 | //unsigned char tag; 258 | // 259 | //if (mlen_p != NULL) { 260 | //*mlen_p = 0U; 261 | //} 262 | //if (tag_p != NULL) { 263 | //*tag_p = 0xff; 264 | //} 265 | 266 | //if (inlen < crypto_secretstream_xchacha20poly1305_ABYTES) { 267 | //return -1; 268 | //} 269 | if inlen < StreamABytes { 270 | return nil, 0, invalidInput 271 | } 272 | //mlen = inlen - crypto_secretstream_xchacha20poly1305_ABYTES; 273 | mlen := inlen - StreamABytes 274 | 275 | //if (mlen > crypto_secretstream_xchacha20poly1305_MESSAGEBYTES_MAX) { 276 | //sodium_misuse(); 277 | //} 278 | 279 | chacha, err := chacha20.NewUnauthenticatedCipher(s.k[:], s.nonce[:]) 280 | if err != nil { 281 | return nil, 0, err 282 | } 283 | //crypto_stream_chacha20_ietf(block, sizeof block, state->nonce, state->k); 284 | chacha.XORKeyStream(block[:], block[:]) 285 | 286 | //crypto_onetimeauth_poly1305_init(&poly1305_state, block); 287 | var poly_init [32]byte 288 | copy(poly_init[:], block[:]) 289 | poly := poly1305.New(&poly_init) 290 | 291 | // TODO 292 | //sodium_memzero(block, sizeof block); 293 | //crypto_onetimeauth_poly1305_update(&poly1305_state, ad, adlen); 294 | //crypto_onetimeauth_poly1305_update(&poly1305_state, _pad0, 295 | //(0x10 - adlen) & 0xf); 296 | // 297 | 298 | //memset(block, 0, sizeof block); 299 | memzero(block[:]) 300 | //block[0] = in[0]; 301 | block[0] = in[0] 302 | 303 | //crypto_stream_chacha20_ietf_xor_ic(block, block, sizeof block, state->nonce, 1U, state->k); 304 | chacha.XORKeyStream(block[:], block[:]) 305 | //tag = block[0]; 306 | tag := block[0] 307 | //block[0] = in[0]; 308 | block[0] = in[0] 309 | //crypto_onetimeauth_poly1305_update(&poly1305_state, block, sizeof block); 310 | if _, err = poly.Write(block[:]); err != nil { 311 | return nil, 0, err 312 | } 313 | 314 | // 315 | //c = in + (sizeof tag); 316 | c := in[1:] 317 | //crypto_onetimeauth_poly1305_update(&poly1305_state, c, mlen); 318 | if _, err = poly.Write(c[:mlen]); err != nil { 319 | return nil, 0, err 320 | } 321 | 322 | //crypto_onetimeauth_poly1305_update (&poly1305_state, _pad0, (0x10 - (sizeof block) + mlen) & 0xf); 323 | padlen := (0x10 - len(block) + mlen) & 0xf 324 | if _, err = poly.Write(pad0[:padlen]); err != nil { 325 | return nil, 0, err 326 | } 327 | 328 | // 329 | //STORE64_LE(slen, (uint64_t) adlen); 330 | //crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen); 331 | binary.LittleEndian.PutUint64(slen[:], uint64(0)) 332 | if _, err = poly.Write(slen[:]); err != nil { 333 | return nil, 0, err 334 | } 335 | 336 | //STORE64_LE(slen, (sizeof block) + mlen); 337 | //crypto_onetimeauth_poly1305_update(&poly1305_state, slen, sizeof slen); 338 | binary.LittleEndian.PutUint64(slen[:], uint64(len(block)+mlen)) 339 | if _, err = poly.Write(slen[:]); err != nil { 340 | return nil, 0, err 341 | } 342 | 343 | // 344 | //crypto_onetimeauth_poly1305_final(&poly1305_state, mac); 345 | //sodium_memzero(&poly1305_state, sizeof poly1305_state); 346 | mac := poly.Sum(nil) 347 | // 348 | //stored_mac = c + mlen; 349 | stored_mac := c[mlen:] 350 | //if (sodium_memcmp(mac, stored_mac, sizeof mac) != 0) { 351 | //sodium_memzero(mac, sizeof mac); 352 | //return -1; 353 | //} 354 | if subtle.ConstantTimeCompare(mac, stored_mac) == 0 { 355 | return nil, 0, cryptoFailure 356 | } 357 | // 358 | //crypto_stream_chacha20_ietf_xor_ic(m, c, mlen, state->nonce, 2U, state->k); 359 | m := make([]byte, mlen) 360 | chacha.XORKeyStream(m, c[:mlen]) 361 | 362 | //XOR_BUF(STATE_INONCE(state), mac, crypto_secretstream_xchacha20poly1305_INONCEBYTES); 363 | //sodium_increment(STATE_COUNTER(state), crypto_secretstream_xchacha20poly1305_COUNTERBYTES); 364 | xor_buf(s.nonce[crypto_secretstream_xchacha20poly1305_COUNTERBYTES:], mac) 365 | buf_inc(s.nonce[:crypto_secretstream_xchacha20poly1305_COUNTERBYTES]) 366 | 367 | // TODO 368 | //if ((tag & crypto_secretstream_xchacha20poly1305_TAG_REKEY) != 0 || 369 | //sodium_is_zero(STATE_COUNTER(state), 370 | //crypto_secretstream_xchacha20poly1305_COUNTERBYTES)) { 371 | //crypto_secretstream_xchacha20poly1305_rekey(state); 372 | //} 373 | 374 | //if (mlen_p != NULL) { 375 | //*mlen_p = mlen; 376 | //} 377 | //if (tag_p != NULL) { 378 | //*tag_p = tag; 379 | //} 380 | //return 0; 381 | return m, tag, nil 382 | } 383 | 384 | func memzero(b []byte) { 385 | for i := range b { 386 | b[i] = 0 387 | } 388 | } 389 | 390 | func xor_buf(out, in []byte) { 391 | for i := range out { 392 | out[i] ^= in[i] 393 | } 394 | } 395 | 396 | func buf_inc(n []byte) { 397 | c := 1 398 | 399 | for i := range n { 400 | c += int(n[i]) 401 | n[i] = byte(c) 402 | c >>= 8 403 | } 404 | } 405 | --------------------------------------------------------------------------------