Initial commit
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Jake Walker 2022-08-03 18:01:58 +01:00
commit a258da3678
Signed by: jakew
GPG Key ID: 2B83DC56C147243B
10 changed files with 609 additions and 0 deletions

17
.drone.yml Normal file
View File

@ -0,0 +1,17 @@
---
kind: pipeline
type: kubernetes
name: default
services:
- name: echo-server
image: ghcr.io/will-scargill/echo:latest
steps:
- name: test
image: golang:1.18
commands:
- curl -L https://vh7.uk/9kiw > waitfor.sh && chmod +x waitfor.sh
- ./waitfor.sh 127.0.0.1:16000 -t 60
- go test -v ./...

139
.gitignore vendored Normal file
View File

@ -0,0 +1,139 @@
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Go Patch ###
/vendor/
/Godeps/
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# Support for Project snippet scope
.vscode/*.code-snippets
# Ignore code-workspaces
*.code-workspace

78
crypto/aes.go Normal file
View File

@ -0,0 +1,78 @@
package crypto
import (
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
)
func packCipherData(cipherText, iv []byte) ([]byte, error) {
return json.Marshal([]string{
base64.StdEncoding.EncodeToString(cipherText),
base64.StdEncoding.EncodeToString(iv),
})
}
func unpackCipherData(data []byte) ([]byte, []byte, error) {
var cipherData []string
err := json.Unmarshal(data, &cipherData)
if err != nil {
return nil, nil, err
}
if len(cipherData) != 2 {
return nil, nil, fmt.Errorf("invalid cipher data")
}
cipherText, err := base64.StdEncoding.DecodeString(cipherData[0])
if err != nil {
return nil, nil, err
}
iv, err := base64.StdEncoding.DecodeString(cipherData[1])
if err != nil {
return nil, nil, err
}
return cipherText, iv, nil
}
func EncryptAesCbc(aes cipher.Block, plainTextBytes []byte) ([]byte, error) {
iv := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
encrypter := cipher.NewCBCEncrypter(aes, iv)
plainTextBytes, err := pkcs7Pad(plainTextBytes, encrypter.BlockSize())
if err != nil {
return nil, err
}
cipherText := make([]byte, len(plainTextBytes))
encrypter.CryptBlocks(cipherText, plainTextBytes)
return packCipherData(cipherText, iv)
}
func DecryptAesCbc(aes cipher.Block, data []byte) ([]byte, error) {
encrypted, iv, err := unpackCipherData(data)
if err != nil {
return nil, err
}
decryptor := cipher.NewCBCDecrypter(aes, iv)
decryptedBytes := make([]byte, len(encrypted))
decryptor.CryptBlocks(decryptedBytes, encrypted)
decryptedBytes, err = pkcs7Unpad(decryptedBytes, decryptor.BlockSize())
if err != nil {
return nil, err
}
return decryptedBytes, nil
}

44
crypto/padding.go Normal file
View File

@ -0,0 +1,44 @@
package crypto
import (
"bytes"
"fmt"
)
// ref: https://golang-examples.tumblr.com/post/98350728789/pkcs7-padding
// Appends padding.
func pkcs7Pad(data []byte, blocklen int) ([]byte, error) {
if blocklen <= 0 {
return nil, fmt.Errorf("Invalid block length %d", blocklen)
}
padlen := 1
for ((len(data) + padlen) % blocklen) != 0 {
padlen = padlen + 1
}
pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
return append(data, pad...), nil
}
// Returns slice of the original data without padding.
func pkcs7Unpad(data []byte, blocklen int) ([]byte, error) {
if blocklen <= 0 {
return nil, fmt.Errorf("Invalid block length %d", blocklen)
}
if len(data)%blocklen != 0 || len(data) == 0 {
return nil, fmt.Errorf("Invalid data length %d", len(data))
}
padlen := int(data[len(data)-1])
if padlen > blocklen || padlen == 0 {
return nil, fmt.Errorf("Invalid padding")
}
// check padding
pad := data[len(data)-padlen:]
for i := 0; i < padlen; i++ {
if pad[i] != byte(padlen) {
return nil, fmt.Errorf("Invalid padding")
}
}
return data[:len(data)-padlen], nil
}

24
crypto/rsa.go Normal file
View File

@ -0,0 +1,24 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/pem"
)
func RsaEncrypt(pubKey []byte, text []byte) (string, error) {
block, _ := pem.Decode(pubKey)
key, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", err
}
k := key.(*rsa.PublicKey)
ciphertext, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, k, text, nil)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.vh7.uk/jakew/echo-go
go 1.18

0
go.sum Normal file
View File

232
main.go Normal file
View File

@ -0,0 +1,232 @@
package echo_go
import (
"bytes"
"crypto/aes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net"
"strings"
"git.vh7.uk/jakew/echo-go/crypto"
)
const EchoVersion = "3.17"
func randomHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func (c *Client) SendPlain(msgType MessageType, data *string, subType *string, metadata []string) error {
metadataBytes, err := json.Marshal(&metadata)
if err != nil {
return err
}
msg := RawMessage{
UserId: c.UserId,
MessageType: msgType,
SubType: subType,
Data: data,
Metadata: string(metadataBytes),
}
b, err := json.Marshal(&msg)
if err != nil {
return err
}
//log.Printf("sending message %v", msg)
_, err = c.Con.Write(b)
return err
}
func (c *Client) Send(msgType MessageType, data *string, subType *string, metadata []string) error {
metadataBytes, err := json.Marshal(&metadata)
if err != nil {
return err
}
msg := RawMessage{
UserId: c.UserId,
MessageType: msgType,
SubType: subType,
Data: data,
Metadata: string(metadataBytes),
}
plainBytes, err := json.Marshal(&msg)
if err != nil {
return err
}
aesCipher, err := aes.NewCipher([]byte(c.SessionKey))
if err != nil {
return err
}
encryptedBytes, err := crypto.EncryptAesCbc(aesCipher, plainBytes)
if err != nil {
return err
}
_, err = c.Con.Write(encryptedBytes)
return err
}
func (c *Client) ReceivePlain() ([]RawMessage, error) {
data := make([]byte, 20480)
_, err := c.Con.Read(data)
if err != nil {
return nil, err
}
messages := []RawMessage{}
for _, message := range strings.Split(string(bytes.Trim(data, "\x00")), "}") {
if strings.TrimSpace(message) == "" {
continue
}
var msg RawMessage
err = json.Unmarshal([]byte(message+"}"), &msg)
if err != nil {
return nil, err
}
messages = append(messages, msg)
}
return messages, nil
}
func (c *Client) Receive() ([]RawMessage, error) {
data := make([]byte, 20480)
_, err := c.Con.Read(data)
if err != nil {
return nil, err
}
messages := []RawMessage{}
for _, message := range strings.Split(string(bytes.Trim(data, "\x00")), "]") {
if strings.TrimSpace(message) == "" {
continue
}
aesCipher, err := aes.NewCipher([]byte(c.SessionKey))
if err != nil {
return nil, err
}
decrypted, err := crypto.DecryptAesCbc(aesCipher, []byte(message+"]"))
if err != nil {
return nil, err
}
var msg RawMessage
err = json.Unmarshal(decrypted, &msg)
if err != nil {
return nil, err
}
messages = append(messages, msg)
}
return messages, nil
}
func (c *Client) handshakeLoop(password string) error {
log.Println("sending server info request")
err := c.SendPlain(ReqServerInfo, nil, nil, nil)
if err != nil {
return err
}
encrypted := false
for {
var msgs []RawMessage
var err error
if encrypted {
msgs, err = c.Receive()
} else {
msgs, err = c.ReceivePlain()
}
if err != nil {
return err
}
for _, msg := range msgs {
switch msg.MessageType {
case ResServerInfo:
ciphertext, err := crypto.RsaEncrypt([]byte(*msg.Data), []byte(c.SessionKey))
err = c.SendPlain(ReqClientSecret, &ciphertext, nil, nil)
if err != nil {
return err
}
encrypted = true
case ResClientSecret:
data, err := json.Marshal([]string{
c.Username,
password,
EchoVersion,
})
if err != nil {
return err
}
dataStr := string(data)
err = c.Send(ReqConnection, &dataStr, nil, nil)
if err != nil {
return err
}
case ResConnectionAccepted:
log.Println("handshake accepted")
return nil
case ResConnectionDenied:
return fmt.Errorf("handshake failed - %v", *msg.Data)
default:
return fmt.Errorf("unexpected handshake message type %v", msg.MessageType)
}
}
}
}
func (c *Client) Disconnect() {
log.Println("gracefully disconnecting")
_ = c.Send(ReqDisconnect, nil, nil, nil)
_ = c.Con.Close()
}
func New(addr string, username string) (*Client, error) {
userId, err := randomHex(32)
if err != nil {
return nil, err
}
sessionKey, err := randomHex(8)
if err != nil {
return nil, err
}
log.Printf("connecting to %v", addr)
con, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
client := Client{
Con: con,
UserId: userId,
SessionKey: sessionKey,
Username: username,
}
return &client, nil
}

17
main_test.go Normal file
View File

@ -0,0 +1,17 @@
package echo_go
import "testing"
func TestCanConnect(t *testing.T) {
client, err := New("127.0.0.1:16000", "EchoGoTest")
if err != nil {
t.Fatalf("failed to create client: %v", err)
}
err = client.handshakeLoop("mypassword")
if err != nil {
t.Fatalf("failed to run handshake loop: %v", err)
}
client.Disconnect()
}

55
types.go Normal file
View File

@ -0,0 +1,55 @@
package echo_go
import (
"fmt"
"net"
)
type MessageType string
const (
ReqServerInfo MessageType = "serverInfoRequest"
ReqClientSecret = "clientSecret"
ReqConnection = "connectionRequest"
ReqDisconnect = "disconnect"
ReqInfo = "requestInfo"
ReqUserMessage = "userMessage"
ReqChangeChannel = "changeChannel"
ReqHistory = "historyRequest"
ReqLeaveChannel = "leaveChannel"
ResServerInfo = "serverInfo"
ResClientSecret = "gotSecret"
ResConnectionAccepted = "CRAccepted"
ResConnectionDenied = "CRDenied"
ResOutboundMessage = "outboundMessage"
ResConnectionTerminated = "connectionTerminated"
ResChannelUpdate = "channelUpdate"
ResCommandData = "commandData"
ResChannelHistory = "channelHistory"
ResErrorOccurred = "errorOccured"
ResAdditionalHistory = "additionalHistory"
)
type Client struct {
Con net.Conn
UserId string
SessionKey string
Username string
}
type RawMessage struct {
UserId string `json:"userid"`
MessageType MessageType `json:"messagetype"`
SubType *string `json:"subtype"`
Data *string `json:"data"`
Metadata string `json:"metadata"`
}
func (m *RawMessage) String() string {
return fmt.Sprintf("\n"+
" UserId: %v\n"+
" Type: %v - %v\n"+
" Data: %v\n"+
" Metadata: %v", m.UserId, m.MessageType, *m.SubType, *m.Data, m.Metadata)
}