Initial commit: crypto.go and protocol.go implementation + unit tests
This commit is contained in:
209
crypto.go
Normal file
209
crypto.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package ownwire_sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/hkdf"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
hkdf_info_prefix = "ownwire/v1:"
|
||||||
|
nonce_label = "ownwire/v1:gcm-nonce"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Keypair struct {
|
||||||
|
ClientPriv *ecdh.PrivateKey
|
||||||
|
ClientPub []byte // 65 bytes, uncompressed
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenClientKey() (Keypair, error) {
|
||||||
|
curve := ecdh.P256()
|
||||||
|
|
||||||
|
client_priv, err := curve.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return Keypair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client_pub := client_priv.PublicKey().Bytes()
|
||||||
|
if len(client_pub) != 65 {
|
||||||
|
return Keypair{}, fmt.Errorf("unexpected P-256 pubkey length: %d", len(client_pub))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Keypair{
|
||||||
|
ClientPriv: client_priv,
|
||||||
|
ClientPub: client_pub,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveSharedKey does:
|
||||||
|
// ECDH -> 32 bytes shared secret
|
||||||
|
// HKDF-SHA256(salt, info="ownwire/v1:<session_id>") -> 32 bytes
|
||||||
|
func DeriveSharedKey(session_id string, client_priv *ecdh.PrivateKey, server_pub_raw []byte, salt []byte) ([32]byte, error) {
|
||||||
|
var out [32]byte
|
||||||
|
|
||||||
|
if client_priv == nil {
|
||||||
|
return out, fmt.Errorf("client_priv is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
curve := ecdh.P256()
|
||||||
|
server_pub, err := curve.NewPublicKey(server_pub_raw)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("invalid server pubkey: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shared_secret, err := client_priv.ECDH(server_pub)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("ecdh failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info_str := hkdf_info_prefix + session_id
|
||||||
|
prk, err := hkdf.Extract(sha256.New, shared_secret, salt)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("hkdf extract failed: %w", err)
|
||||||
|
}
|
||||||
|
okm, err := hkdf.Expand(sha256.New, prk, info_str, 32)
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("hkdf expand failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(out[:], okm)
|
||||||
|
|
||||||
|
zero_bytes(shared_secret)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeriveNonce computes:
|
||||||
|
// HMAC-SHA256(key=shared_key, data=label + uuid_bytes + salt16 + seq_be8 + flag)
|
||||||
|
// IV = first 12 bytes
|
||||||
|
func DeriveNonce(shared_key [32]byte, session_uuid_bytes [16]byte, salt16 [16]byte, seq_num uint64, is_response bool) [12]byte {
|
||||||
|
var iv [12]byte
|
||||||
|
|
||||||
|
seq_be8 := [8]byte{}
|
||||||
|
for i := 7; i >= 0; i-- {
|
||||||
|
seq_be8[i] = byte(seq_num & 0xff)
|
||||||
|
seq_num >>= 8
|
||||||
|
}
|
||||||
|
|
||||||
|
flag := byte(0)
|
||||||
|
if is_response {
|
||||||
|
flag = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, shared_key[:])
|
||||||
|
mac.Write([]byte(nonce_label))
|
||||||
|
mac.Write(session_uuid_bytes[:])
|
||||||
|
mac.Write(salt16[:])
|
||||||
|
mac.Write(seq_be8[:])
|
||||||
|
mac.Write([]byte{flag})
|
||||||
|
|
||||||
|
sum := mac.Sum(nil) // 32 bytes
|
||||||
|
copy(iv[:], sum[:12])
|
||||||
|
return iv
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedPayload struct {
|
||||||
|
ContentB64 string
|
||||||
|
SaltHex string
|
||||||
|
}
|
||||||
|
|
||||||
|
func EncryptAESGCM(shared_key [32]byte, session_uuid_bytes [16]byte, plain_text []byte, seq_num uint64, is_response bool) (EncryptedPayload, error) {
|
||||||
|
var salt16 [16]byte
|
||||||
|
if _, err := rand.Read(salt16[:]); err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := DeriveNonce(shared_key, session_uuid_bytes, salt16, seq_num, is_response)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(shared_key[:])
|
||||||
|
if err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return EncryptedPayload{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ct := aead.Seal(nil, iv[:], plain_text, nil)
|
||||||
|
|
||||||
|
return EncryptedPayload{
|
||||||
|
ContentB64: base64.StdEncoding.EncodeToString(ct),
|
||||||
|
SaltHex: hex.EncodeToString(salt16[:]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptAESGCM(shared_key [32]byte, session_uuid_bytes [16]byte, content_b64 string, salt_hex string, seq_num uint64, is_response bool) ([]byte, error) {
|
||||||
|
ct, err := base64.StdEncoding.DecodeString(content_b64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid content base64: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
salt_raw, err := hex.DecodeString(salt_hex)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid salt hex: %w", err)
|
||||||
|
}
|
||||||
|
if len(salt_raw) != 16 {
|
||||||
|
return nil, fmt.Errorf("invalid salt length: %d", len(salt_raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
var salt16 [16]byte
|
||||||
|
copy(salt16[:], salt_raw)
|
||||||
|
|
||||||
|
iv := DeriveNonce(shared_key, session_uuid_bytes, salt16, seq_num, is_response)
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(shared_key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aead, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pt, err := aead.Open(nil, iv[:], ct, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseUUIDBytes parses "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" into 16 bytes.
|
||||||
|
func ParseUUIDBytes(uuid_str string) ([16]byte, error) {
|
||||||
|
var out [16]byte
|
||||||
|
|
||||||
|
clean := make([]byte, 0, 32)
|
||||||
|
for i := 0; i < len(uuid_str); i++ {
|
||||||
|
b := uuid_str[i]
|
||||||
|
if b == '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clean = append(clean, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(clean) != 32 {
|
||||||
|
return out, fmt.Errorf("invalid uuid: %q", uuid_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded, err := hex.DecodeString(string(clean))
|
||||||
|
if err != nil {
|
||||||
|
return out, fmt.Errorf("invalid uuid hex: %w", err)
|
||||||
|
}
|
||||||
|
copy(out[:], decoded)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func zero_bytes(b []byte) {
|
||||||
|
for i := range b {
|
||||||
|
b[i] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
108
crypto_test.go
Normal file
108
crypto_test.go
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package ownwire_sdk_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
sdk "ownwire.net/ownwire-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func derive_nonce_reference(shared_key [32]byte, session_uuid_bytes [16]byte, salt16 [16]byte, seq_num uint64, is_response bool) [12]byte {
|
||||||
|
seq_be8 := [8]byte{}
|
||||||
|
for i := 7; i >= 0; i-- {
|
||||||
|
seq_be8[i] = byte(seq_num & 0xff)
|
||||||
|
seq_num >>= 8
|
||||||
|
}
|
||||||
|
|
||||||
|
flag := byte(0)
|
||||||
|
if is_response {
|
||||||
|
flag = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, shared_key[:])
|
||||||
|
mac.Write([]byte("ownwire/v1:gcm-nonce"))
|
||||||
|
mac.Write(session_uuid_bytes[:])
|
||||||
|
mac.Write(salt16[:])
|
||||||
|
mac.Write(seq_be8[:])
|
||||||
|
mac.Write([]byte{flag})
|
||||||
|
|
||||||
|
sum := mac.Sum(nil)
|
||||||
|
|
||||||
|
var iv [12]byte
|
||||||
|
copy(iv[:], sum[:12])
|
||||||
|
return iv
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("Crypto", func() {
|
||||||
|
It("parses UUID bytes", func() {
|
||||||
|
uuid_str := "cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a"
|
||||||
|
uuid_bytes, err := sdk.ParseUUIDBytes(uuid_str)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(hex.EncodeToString(uuid_bytes[:])).To(Equal("cb653f536f7d4aebba0dd2b17c290d8a"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("derives nonce exactly as reference implementation", func() {
|
||||||
|
var shared_key [32]byte
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
shared_key[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid_bytes, err := sdk.ParseUUIDBytes("cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
var salt16 [16]byte
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
salt16[i] = byte(0xa0 + i)
|
||||||
|
}
|
||||||
|
|
||||||
|
seq_num := uint64(0x0102030405060708)
|
||||||
|
|
||||||
|
got := sdk.DeriveNonce(shared_key, uuid_bytes, salt16, seq_num, false)
|
||||||
|
want := derive_nonce_reference(shared_key, uuid_bytes, salt16, seq_num, false)
|
||||||
|
|
||||||
|
Expect(got).To(Equal(want))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("encrypts and decrypts roundtrip", func() {
|
||||||
|
var shared_key [32]byte
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
shared_key[i] = byte(0x55 ^ i)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid_bytes, err := sdk.ParseUUIDBytes("cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
plain_text := []byte("hello ownwire")
|
||||||
|
seq_num := uint64(123)
|
||||||
|
|
||||||
|
enc, err := sdk.EncryptAESGCM(shared_key, uuid_bytes, plain_text, seq_num, false)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(enc.ContentB64).ToNot(BeEmpty())
|
||||||
|
Expect(enc.SaltHex).To(HaveLen(32)) // 16 bytes hex
|
||||||
|
|
||||||
|
out, err := sdk.DecryptAESGCM(shared_key, uuid_bytes, enc.ContentB64, enc.SaltHex, seq_num, false)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(string(out)).To(Equal(string(plain_text)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("fails decrypt if seq changes", func() {
|
||||||
|
var shared_key [32]byte
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
shared_key[i] = byte(0x11 + i)
|
||||||
|
}
|
||||||
|
|
||||||
|
uuid_bytes, err := sdk.ParseUUIDBytes("cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a")
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
enc, err := sdk.EncryptAESGCM(shared_key, uuid_bytes, []byte("hello"), 1, false)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
|
||||||
|
_, err = sdk.DecryptAESGCM(shared_key, uuid_bytes, enc.ContentB64, enc.SaltHex, 2, false)
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
23
go.mod
Normal file
23
go.mod
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
module ownwire.net/ownwire-sdk
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.3
|
||||||
|
github.com/onsi/gomega v1.38.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
|
golang.org/x/net v0.43.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
)
|
||||||
69
go.sum
Normal file
69
go.sum
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||||
|
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
|
||||||
|
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
|
||||||
|
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE=
|
||||||
|
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||||
|
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
|
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
|
||||||
|
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||||
|
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||||
|
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
|
||||||
|
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||||
|
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||||
|
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
|
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||||
|
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||||
|
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||||
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
|
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||||
|
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
14
ownwire_sdk_suite_test.go
Normal file
14
ownwire_sdk_suite_test.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package ownwire_sdk_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOwnwireSdk(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Ownwire SDK Suite")
|
||||||
|
}
|
||||||
|
|
||||||
47
protocol.go
Normal file
47
protocol.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package ownwire_sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SessionInit struct {
|
||||||
|
SessionId string
|
||||||
|
ServerPubKeyB64 string
|
||||||
|
SaltB64 string
|
||||||
|
SeqOut uint64
|
||||||
|
SeqInMax uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseSessionInit parses:
|
||||||
|
// "/session:<id>:<server_pubkey_b64>:<salt_b64>:<seq_out>:<seq_in_max>"
|
||||||
|
func ParseSessionInit(line string) (SessionInit, error) {
|
||||||
|
if !strings.HasPrefix(line, "/session:") {
|
||||||
|
return SessionInit{}, fmt.Errorf("invalid session line prefix: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, ":")
|
||||||
|
if len(parts) < 6 || parts[0] != "/session" {
|
||||||
|
return SessionInit{}, fmt.Errorf("invalid session line: %q", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
seq_out, err := strconv.ParseUint(parts[4], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return SessionInit{}, fmt.Errorf("invalid seq_out: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seq_in_max, err := strconv.ParseUint(parts[5], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return SessionInit{}, fmt.Errorf("invalid seq_in_max: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := SessionInit{
|
||||||
|
SessionId: parts[1],
|
||||||
|
ServerPubKeyB64: parts[2],
|
||||||
|
SaltB64: parts[3],
|
||||||
|
SeqOut: seq_out,
|
||||||
|
SeqInMax: seq_in_max,
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
36
protocol_test.go
Normal file
36
protocol_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package ownwire_sdk_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
sdk "ownwire.net/ownwire-sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("ParseSessionInit", func() {
|
||||||
|
It("parses a valid /session line", func() {
|
||||||
|
line := "/session:cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a:SERVERPUBB64:SALTB64:12:34"
|
||||||
|
parsed, err := sdk.ParseSessionInit(line)
|
||||||
|
Expect(err).To(BeNil())
|
||||||
|
Expect(parsed.SessionId).To(Equal("cb653f53-6f7d-4aeb-ba0d-d2b17c290d8a"))
|
||||||
|
Expect(parsed.ServerPubKeyB64).To(Equal("SERVERPUBB64"))
|
||||||
|
Expect(parsed.SaltB64).To(Equal("SALTB64"))
|
||||||
|
Expect(parsed.SeqOut).To(Equal(uint64(12)))
|
||||||
|
Expect(parsed.SeqInMax).To(Equal(uint64(34)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects non-session lines", func() {
|
||||||
|
_, err := sdk.ParseSessionInit("/wat:1:2:3:4:5")
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects missing fields", func() {
|
||||||
|
_, err := sdk.ParseSessionInit("/session:1:2:3")
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects bad sequence numbers", func() {
|
||||||
|
_, err := sdk.ParseSessionInit("/session:1:2:3:nope:5")
|
||||||
|
Expect(err).ToNot(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user