Made the default client connect to channel 0 and stream audio data over the raw channel
							parent
							
								
									37d42b5a9b
								
							
						
					
					
						commit
						4a2ce02617
					
				| @ -1,22 +1,79 @@ | ||||
| package pnyx | ||||
| 
 | ||||
| import ( | ||||
| 	"slices" | ||||
| ) | ||||
| 
 | ||||
| type ChannelID uint32 | ||||
| 
 | ||||
| const RootChannelID = 0 | ||||
| const ( | ||||
|   RootChannelID ChannelID = 0 | ||||
| 
 | ||||
|   MODE_RAW ModeID = iota | ||||
| 
 | ||||
|   MODE_COMMAND_DATA byte = 0x00 | ||||
|   MODE_COMMAND_JOIN = 0x01 | ||||
|   MODE_COMMAND_LEAVE = 0x02 | ||||
| ) | ||||
| 
 | ||||
| type ModeID uint8 | ||||
| type CommandID uint8 | ||||
| 
 | ||||
| type PermissionMap map[PeerID]map[ModeID]map[CommandID]bool | ||||
| 
 | ||||
| type Channel struct { | ||||
|   modes map[ModeID]Mode | ||||
|   permissions PermissionMap | ||||
|   parent ChannelID | ||||
| } | ||||
| 
 | ||||
| type SendPacket struct { | ||||
|   Packet *Packet | ||||
|   Session SessionID | ||||
| } | ||||
| 
 | ||||
| type Mode interface { | ||||
|   // Process takes incoming packets from a session and returns a list of packets to send
 | ||||
|   Process(*Session, *Packet) []SendPacket | ||||
| } | ||||
| 
 | ||||
| func multiplex(session *Session, packet *Packet, sessions []SessionID) []SendPacket { | ||||
|   send_packets := make([]SendPacket, len(sessions)) | ||||
|   for i, session_id := range(sessions) { | ||||
|     if session_id == session.ID { | ||||
|       continue | ||||
|     } | ||||
| 
 | ||||
|     send_packets[i] = SendPacket{ | ||||
|       Packet: packet, | ||||
|       Session: session_id, | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return send_packets | ||||
| } | ||||
| 
 | ||||
| type RawMode struct { | ||||
|   Sessions []SessionID | ||||
| } | ||||
| 
 | ||||
| func(mode *RawMode) Process(session *Session, packet *Packet) []SendPacket { | ||||
|   switch packet.Command { | ||||
|   case MODE_COMMAND_JOIN: | ||||
|     if slices.Contains(mode.Sessions, session.ID) == false { | ||||
|       mode.Sessions = append(mode.Sessions, session.ID) | ||||
|     } | ||||
|   case MODE_COMMAND_LEAVE: | ||||
|     idx := slices.Index(mode.Sessions, session.ID) | ||||
|     if idx != -1 { | ||||
|       mode.Sessions = slices.Delete(mode.Sessions, idx, idx+1) | ||||
|     } | ||||
|   case MODE_COMMAND_DATA: | ||||
|     if slices.Contains(mode.Sessions, session.ID) { | ||||
|       new_packet := &Packet{ | ||||
|         Channel: packet.Channel, | ||||
|         Mode: packet.Mode, | ||||
|         Command: MODE_COMMAND_DATA, | ||||
|         Data: append(session.Peer[:], packet.Data...), | ||||
|       } | ||||
|       return multiplex(session, new_packet, mode.Sessions) | ||||
|     } | ||||
|   } | ||||
|   return nil | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,13 @@ | ||||
| module git.metznet.ca/MetzNet/pnyx | ||||
| 
 | ||||
| go 1.21.5 | ||||
| go 1.22.0 | ||||
| 
 | ||||
| require ( | ||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | ||||
| 	github.com/ebitengine/purego v0.7.1 // indirect | ||||
| 	github.com/gen2brain/malgo v0.11.21 // indirect | ||||
| 	github.com/google/uuid v1.6.0 // indirect | ||||
| 	github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 // indirect | ||||
| 	github.com/pion/opus v0.0.0-20240403022900-1c7b6eddc7c9 // indirect | ||||
| 	golang.org/x/sys v0.7.0 // indirect | ||||
| ) | ||||
|  | ||||
| @ -1,4 +1,14 @@ | ||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||
| github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= | ||||
| github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= | ||||
| github.com/gen2brain/malgo v0.11.21 h1:qsS4Dh6zhZgmvAW5CtKRxDjQzHbc2NJlBG9eE0tgS8w= | ||||
| github.com/gen2brain/malgo v0.11.21/go.mod h1:f9TtuN7DVrXMiV/yIceMeWpvanyVzJQMlBecJFVMxww= | ||||
| github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||
| github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5 h1:5AlozfqaVjGYGhms2OsdUyfdJME76E6rx5MdGpjzZpc= | ||||
| github.com/gordonklaus/portaudio v0.0.0-20230709114228-aafa478834f5/go.mod h1:WY8R6YKlI2ZI3UyzFk7P6yGSuS+hFwNtEzrexRyD7Es= | ||||
| github.com/pion/opus v0.0.0-20240403022900-1c7b6eddc7c9 h1:/aqYkFcwlpZVXSt1cLDXppeDQlABu9zZq/mBVX3v/5w= | ||||
| github.com/pion/opus v0.0.0-20240403022900-1c7b6eddc7c9/go.mod h1:APGXJHkH8qlbefy7R7/i6a2w/nvXC85hnHm8FjaGgMo= | ||||
| golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= | ||||
| golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
|  | ||||
| @ -1,264 +1,37 @@ | ||||
| package pnyx | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/aes" | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/ed25519" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/binary" | ||||
| 	"hash/crc32" | ||||
| 	"io" | ||||
| 	mrand "math/rand" | ||||
| 	"net" | ||||
| 
 | ||||
| 	"fmt" | ||||
| 	"slices" | ||||
| 
 | ||||
| 	"filippo.io/edwards25519" | ||||
| ) | ||||
| 
 | ||||
| type PacketType uint8 | ||||
| const ( | ||||
|   ID_LENGTH = 16 | ||||
|   IV_LENGTH = aes.BlockSize | ||||
|   PUBKEY_LENGTH = 32 | ||||
|   ECDH_LENGTH = 32 | ||||
|   SIGNATURE_LENGTH = 64 | ||||
|   HMAC_LENGTH = 64 | ||||
|   COMMAND_LENGTH = 1 | ||||
| 
 | ||||
|   SESSION_OPEN_LENGTH = PUBKEY_LENGTH + ECDH_LENGTH + SIGNATURE_LENGTH | ||||
|   SESSION_CONNECT_LENGTH = 2 + HMAC_LENGTH // + return addr string length
 | ||||
|   SESSION_CLOSE_LENGTH = HMAC_LENGTH | ||||
| 
 | ||||
|   SESSION_OPEN PacketType = iota | ||||
|   SESSION_CONNECT | ||||
|   SESSION_CLOSE | ||||
|   SESSION_DATA | ||||
|   "encoding/binary" | ||||
|   "fmt" | ||||
| ) | ||||
| 
 | ||||
| func ECDH(public ed25519.PublicKey, private ed25519.PrivateKey) ([]byte, error) { | ||||
|   public_point, err := (&edwards25519.Point{}).SetBytes(public) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   }  | ||||
| 
 | ||||
| 
 | ||||
|   h := sha512.Sum512(private.Seed()) | ||||
|   private_scalar, err := (&edwards25519.Scalar{}).SetBytesWithClamping(h[:32]) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   } | ||||
| 
 | ||||
|   shared_point := public_point.ScalarMult(private_scalar, public_point) | ||||
| 
 | ||||
|   return shared_point.BytesMontgomery(), nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error) { | ||||
|   if key == nil { | ||||
|     return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key") | ||||
|   } | ||||
| 
 | ||||
|   public, private, err := ed25519.GenerateKey(rand.Reader) | ||||
|   if err != nil { | ||||
|     return nil, nil, fmt.Errorf("Failed to generate ecdh key: %w", err) | ||||
|   } | ||||
| 
 | ||||
|   packet := make([]byte, COMMAND_LENGTH + SESSION_OPEN_LENGTH) | ||||
|   cur := 0 | ||||
| 
 | ||||
|   packet[0] = byte(SESSION_OPEN) | ||||
|   cur += COMMAND_LENGTH | ||||
| 
 | ||||
|   copy(packet[cur:], []byte(key.Public().(ed25519.PublicKey))) | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   copy(packet[cur:], []byte(public))  | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   signature := ed25519.Sign(key, packet[COMMAND_LENGTH:cur]) | ||||
|   copy(packet[cur:], signature) | ||||
|   cur += SIGNATURE_LENGTH | ||||
| 
 | ||||
|   return packet, private, nil | ||||
| } | ||||
| 
 | ||||
| func ParseSessionOpen(ecdh ed25519.PrivateKey, session_open []byte) (Session, error) { | ||||
|   if len(session_open) != SESSION_OPEN_LENGTH { | ||||
|     return Session{}, fmt.Errorf("Bad SESSION_OPEN length: %d/%d", len(session_open), SESSION_OPEN_LENGTH) | ||||
|   } | ||||
| 
 | ||||
|   cur := 0 | ||||
| 
 | ||||
|   client_pubkey := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   client_ecdh := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) | ||||
|   cur += PUBKEY_LENGTH | ||||
|    | ||||
|   digest := session_open[:cur] | ||||
|   signature := session_open[cur:cur+SIGNATURE_LENGTH] | ||||
|   cur += SIGNATURE_LENGTH | ||||
| 
 | ||||
|   if ed25519.Verify(client_pubkey, digest, signature) == false { | ||||
|     return Session{}, fmt.Errorf("SESSION_OPEN signature verification failed") | ||||
|   } | ||||
| 
 | ||||
|   session_secret, err := ECDH(client_ecdh, ecdh) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } | ||||
| 
 | ||||
|   session_id := ID[SessionID](session_secret) | ||||
|   client_id := ID[PeerID](client_pubkey) | ||||
| 
 | ||||
|   session_cipher, err := aes.NewCipher(session_secret) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } | ||||
| 
 | ||||
|   seed_bytes := make([]byte, 8) | ||||
|   read, err := rand.Read(seed_bytes) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } else if read != 8 { | ||||
|     return Session{}, fmt.Errorf("Not enough entropy to create session") | ||||
|   } | ||||
| 
 | ||||
|   return Session{ | ||||
|     ID: session_id, | ||||
|     remote: nil, | ||||
|     Peer: client_id, | ||||
|     secret: session_secret, | ||||
|     cipher: session_cipher, | ||||
|     iv_generator: mrand.NewSource(int64(binary.BigEndian.Uint64(seed_bytes))).(mrand.Source64), | ||||
|   }, nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionConnect(address *net.UDPAddr, session_secret []byte) []byte { | ||||
|   packet := make([]byte, COMMAND_LENGTH + ID_LENGTH + SESSION_CONNECT_LENGTH + len(address.String())) | ||||
|   cur := 0 | ||||
| 
 | ||||
|   packet[cur] = byte(SESSION_CONNECT) | ||||
|   cur += COMMAND_LENGTH | ||||
| 
 | ||||
|   session_id := [16]byte(ID[SessionID](session_secret)) | ||||
|   copy(packet[cur:], session_id[:]) | ||||
|   cur += ID_LENGTH | ||||
| 
 | ||||
|   binary.BigEndian.PutUint16(packet[cur:], uint16(len(address.String()))) | ||||
|   cur += 2 | ||||
|    | ||||
|   copy(packet[cur:], []byte(address.String())) | ||||
|   cur += len(address.String()) | ||||
| 
 | ||||
|   hmac := sha512.Sum512(append(packet[COMMAND_LENGTH+ID_LENGTH:cur], session_secret...)) | ||||
|   copy(packet[cur:], hmac[:]) | ||||
| 
 | ||||
|   return packet | ||||
| type Packet struct { | ||||
|   Channel ChannelID | ||||
|   Mode ModeID | ||||
|   Command byte | ||||
|   Data []byte | ||||
| } | ||||
| 
 | ||||
| func ParseSessionConnect(session_connect []byte, session_secret []byte) (*net.UDPAddr, error) { | ||||
|   if len(session_connect) < SESSION_CONNECT_LENGTH { | ||||
|     return nil, fmt.Errorf("Bad session connect length: %d/%d", len(session_connect), SESSION_CONNECT_LENGTH) | ||||
|   } | ||||
| 
 | ||||
|   cur := 0 | ||||
| 
 | ||||
|   address_length := int(binary.BigEndian.Uint16(session_connect[cur:cur+2])) | ||||
|   cur += 2 | ||||
| 
 | ||||
|   if len(session_connect) != (SESSION_CONNECT_LENGTH + address_length) { | ||||
|     return nil, fmt.Errorf("Bad session connect length: %d/%d", len(session_connect), SESSION_CONNECT_LENGTH + address_length) | ||||
|   } | ||||
| 
 | ||||
|   address := string(session_connect[cur:cur+address_length]) | ||||
|   cur += address_length | ||||
| 
 | ||||
|   hmac_digest := make([]byte, cur) | ||||
|   copy(hmac_digest, session_connect[:cur]) | ||||
| 
 | ||||
|   hmac := session_connect[cur:cur+HMAC_LENGTH] | ||||
|   cur += HMAC_LENGTH | ||||
| 
 | ||||
|   calculated_hmac := sha512.Sum512(append(hmac_digest, session_secret...)) | ||||
|   if slices.Compare(hmac, calculated_hmac[:]) != 0 { | ||||
|     return nil, fmt.Errorf("Session connect bad HMAC") | ||||
|   } | ||||
| 
 | ||||
|   addr, err := net.ResolveUDPAddr("udp", address) | ||||
|   if err != nil { | ||||
|     return nil, fmt.Errorf("Error parsing return address: %w", err) | ||||
|   } | ||||
| 
 | ||||
|   return addr, nil | ||||
| func(packet Packet) String() string { | ||||
|   return fmt.Sprintf("{Channel: %x, Mode: %x, Data: %x}", packet.Channel, packet.Mode, packet.Data) | ||||
| } | ||||
| 
 | ||||
| func NewSessionData(session *Session, packet []byte) ([]byte, error) { | ||||
|   iv := make([]byte, IV_LENGTH) | ||||
|   for i := 0; i < IV_LENGTH/8; i++ { | ||||
|     binary.BigEndian.PutUint64(iv[i*8:], session.iv_generator.Uint64()) | ||||
|   } | ||||
| 
 | ||||
|   stream := cipher.NewOFB(session.cipher, iv[:]) | ||||
|   header := make([]byte, COMMAND_LENGTH + ID_LENGTH + IV_LENGTH) | ||||
|   header[0] = byte(SESSION_DATA) | ||||
|   copy(header[COMMAND_LENGTH:], session.ID[:]) | ||||
|   copy(header[COMMAND_LENGTH+ID_LENGTH:], iv) | ||||
| 
 | ||||
|   packet_encrypted := bytes.NewBuffer(header) | ||||
|   writer := &cipher.StreamWriter{S: stream, W: packet_encrypted} | ||||
| 
 | ||||
|   // Encrypt the packet with a crc32 checksum appended to the end
 | ||||
|   _, err := io.Copy(writer, bytes.NewBuffer(binary.BigEndian.AppendUint32(packet, crc32.ChecksumIEEE(packet)))) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   } | ||||
| 
 | ||||
|   return packet_encrypted.Bytes(), nil | ||||
| func(packet Packet) MarshalBinary() ([]byte, error) { | ||||
|   p := binary.BigEndian.AppendUint32(nil, uint32(packet.Channel)) | ||||
|   p = append(p, byte(packet.Mode)) | ||||
|   p = append(p, byte(packet.Command)) | ||||
|   return append(p, packet.Data...), nil  | ||||
| } | ||||
| 
 | ||||
| func ParseSessionData(session *Session, encrypted []byte) ([]byte, error) { | ||||
|   iv := encrypted[0:IV_LENGTH] | ||||
| 
 | ||||
|   stream := cipher.NewOFB(session.cipher, iv) | ||||
|   var packet bytes.Buffer | ||||
|   reader := &cipher.StreamReader{S: stream, R: bytes.NewBuffer(encrypted[IV_LENGTH:])} | ||||
|   _, err := io.Copy(&packet, reader) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
| func ParsePacket(data []byte) (*Packet, error) { | ||||
|   if len(data) < 6 { | ||||
|     return nil, fmt.Errorf("Not enough bytes to parse Packet(%d/%d)", len(data), 6) | ||||
|   } | ||||
| 
 | ||||
|   packet_clear := packet.Bytes() | ||||
|   checksum := binary.BigEndian.Uint32(packet_clear[len(packet_clear)-4:]) | ||||
|   data := packet_clear[:len(packet_clear)-4] | ||||
|   calculated := crc32.ChecksumIEEE(data) | ||||
|   if checksum != calculated { | ||||
|     return nil, fmt.Errorf("SESSION_DATA checksum mismatch: %x != %x", checksum, calculated) | ||||
|   } | ||||
| 
 | ||||
|   return data, nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionClose(session *Session) []byte { | ||||
|   packet := make([]byte, COMMAND_LENGTH + ID_LENGTH + SESSION_CLOSE_LENGTH) | ||||
|   packet[0] = byte(SESSION_CLOSE) | ||||
|   copy(packet[1:], session.ID[:]) | ||||
| 
 | ||||
|   hmac := sha512.Sum512(append(session.ID[:], session.secret...)) | ||||
|   copy(packet[COMMAND_LENGTH + ID_LENGTH:], hmac[:]) | ||||
| 
 | ||||
|   return packet | ||||
| } | ||||
| 
 | ||||
| func ParseSessionClose(session *Session, hmac []byte) error { | ||||
|   calculated_hmac := sha512.Sum512(append(session.ID[:], session.secret...)) | ||||
|   if slices.Compare(hmac, calculated_hmac[:]) != 0 { | ||||
|     return fmt.Errorf("Session Close HMAC validation failed") | ||||
|   } else { | ||||
|     return nil | ||||
|   } | ||||
|   return &Packet{ | ||||
|     Channel: ChannelID(binary.BigEndian.Uint32(data)), | ||||
|     Mode: ModeID(data[4]), | ||||
|     Command: data[5], | ||||
|     Data: data[6:], | ||||
|   }, nil | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,282 @@ | ||||
| package pnyx | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/aes" | ||||
| 	"crypto/cipher" | ||||
| 	"crypto/ed25519" | ||||
| 	"crypto/rand" | ||||
| 	"crypto/sha512" | ||||
| 	"encoding/binary" | ||||
| 	"hash/crc32" | ||||
| 	"io" | ||||
| 	mrand "math/rand" | ||||
| 	"net" | ||||
| 
 | ||||
| 	"fmt" | ||||
| 	"slices" | ||||
| 
 | ||||
| 	"filippo.io/edwards25519" | ||||
| 	"github.com/google/uuid" | ||||
| ) | ||||
| 
 | ||||
| type SessionID uuid.UUID | ||||
| func(id SessionID) String() string { | ||||
|   return uuid.UUID(id).String() | ||||
| } | ||||
| 
 | ||||
| type Session struct { | ||||
|   ID SessionID | ||||
|   remote *net.UDPAddr | ||||
|   Peer PeerID | ||||
|   secret []byte | ||||
|   cipher cipher.Block | ||||
|   iv_generator mrand.Source64 | ||||
| } | ||||
| 
 | ||||
| type PacketType uint8 | ||||
| const ( | ||||
|   ID_LENGTH = 16 | ||||
|   IV_LENGTH = aes.BlockSize | ||||
|   PUBKEY_LENGTH = 32 | ||||
|   ECDH_LENGTH = 32 | ||||
|   SIGNATURE_LENGTH = 64 | ||||
|   HMAC_LENGTH = 64 | ||||
|   COMMAND_LENGTH = 1 | ||||
| 
 | ||||
|   SESSION_OPEN_LENGTH = PUBKEY_LENGTH + ECDH_LENGTH + SIGNATURE_LENGTH | ||||
|   SESSION_CONNECT_LENGTH = 2 + HMAC_LENGTH // + return addr string length
 | ||||
|   SESSION_CLOSE_LENGTH = HMAC_LENGTH | ||||
| 
 | ||||
|   /* | ||||
|     pnyx session packets | ||||
|   */ | ||||
|   SESSION_OPEN PacketType = iota | ||||
|   SESSION_CONNECT | ||||
|   SESSION_CLOSE | ||||
|   SESSION_DATA | ||||
| ) | ||||
| 
 | ||||
| func ECDH(public ed25519.PublicKey, private ed25519.PrivateKey) ([]byte, error) { | ||||
|   public_point, err := (&edwards25519.Point{}).SetBytes(public) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   }  | ||||
| 
 | ||||
| 
 | ||||
|   h := sha512.Sum512(private.Seed()) | ||||
|   private_scalar, err := (&edwards25519.Scalar{}).SetBytesWithClamping(h[:32]) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   } | ||||
| 
 | ||||
|   shared_point := public_point.ScalarMult(private_scalar, public_point) | ||||
| 
 | ||||
|   return shared_point.BytesMontgomery(), nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error) { | ||||
|   if key == nil { | ||||
|     return nil, nil, fmt.Errorf("Cannot create a SESSION_OPEN packet without a key") | ||||
|   } | ||||
| 
 | ||||
|   public, private, err := ed25519.GenerateKey(rand.Reader) | ||||
|   if err != nil { | ||||
|     return nil, nil, fmt.Errorf("Failed to generate ecdh key: %w", err) | ||||
|   } | ||||
| 
 | ||||
|   packet := make([]byte, COMMAND_LENGTH + SESSION_OPEN_LENGTH) | ||||
|   cur := 0 | ||||
| 
 | ||||
|   packet[0] = byte(SESSION_OPEN) | ||||
|   cur += COMMAND_LENGTH | ||||
| 
 | ||||
|   copy(packet[cur:], []byte(key.Public().(ed25519.PublicKey))) | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   copy(packet[cur:], []byte(public))  | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   signature := ed25519.Sign(key, packet[COMMAND_LENGTH:cur]) | ||||
|   copy(packet[cur:], signature) | ||||
|   cur += SIGNATURE_LENGTH | ||||
| 
 | ||||
|   return packet, private, nil | ||||
| } | ||||
| 
 | ||||
| func ParseSessionOpen(ecdh ed25519.PrivateKey, session_open []byte) (Session, error) { | ||||
|   if len(session_open) != SESSION_OPEN_LENGTH { | ||||
|     return Session{}, fmt.Errorf("Bad SESSION_OPEN length: %d/%d", len(session_open), SESSION_OPEN_LENGTH) | ||||
|   } | ||||
| 
 | ||||
|   cur := 0 | ||||
| 
 | ||||
|   client_pubkey := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) | ||||
|   cur += PUBKEY_LENGTH | ||||
| 
 | ||||
|   client_ecdh := (ed25519.PublicKey)(session_open[cur:cur+PUBKEY_LENGTH]) | ||||
|   cur += PUBKEY_LENGTH | ||||
|    | ||||
|   digest := session_open[:cur] | ||||
|   signature := session_open[cur:cur+SIGNATURE_LENGTH] | ||||
|   cur += SIGNATURE_LENGTH | ||||
| 
 | ||||
|   if ed25519.Verify(client_pubkey, digest, signature) == false { | ||||
|     return Session{}, fmt.Errorf("SESSION_OPEN signature verification failed") | ||||
|   } | ||||
| 
 | ||||
|   session_secret, err := ECDH(client_ecdh, ecdh) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } | ||||
| 
 | ||||
|   session_id := ID[SessionID](session_secret) | ||||
|   client_id := ID[PeerID](client_pubkey) | ||||
| 
 | ||||
|   session_cipher, err := aes.NewCipher(session_secret) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } | ||||
| 
 | ||||
|   seed_bytes := make([]byte, 8) | ||||
|   read, err := rand.Read(seed_bytes) | ||||
|   if err != nil { | ||||
|     return Session{}, err | ||||
|   } else if read != 8 { | ||||
|     return Session{}, fmt.Errorf("Not enough entropy to create session") | ||||
|   } | ||||
| 
 | ||||
|   return Session{ | ||||
|     ID: session_id, | ||||
|     remote: nil, | ||||
|     Peer: client_id, | ||||
|     secret: session_secret, | ||||
|     cipher: session_cipher, | ||||
|     iv_generator: mrand.NewSource(int64(binary.BigEndian.Uint64(seed_bytes))).(mrand.Source64), | ||||
|   }, nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionConnect(address *net.UDPAddr, session_secret []byte) []byte { | ||||
|   packet := make([]byte, COMMAND_LENGTH + ID_LENGTH + SESSION_CONNECT_LENGTH + len(address.String())) | ||||
|   cur := 0 | ||||
| 
 | ||||
|   packet[cur] = byte(SESSION_CONNECT) | ||||
|   cur += COMMAND_LENGTH | ||||
| 
 | ||||
|   session_id := [16]byte(ID[SessionID](session_secret)) | ||||
|   copy(packet[cur:], session_id[:]) | ||||
|   cur += ID_LENGTH | ||||
| 
 | ||||
|   binary.BigEndian.PutUint16(packet[cur:], uint16(len(address.String()))) | ||||
|   cur += 2 | ||||
|    | ||||
|   copy(packet[cur:], []byte(address.String())) | ||||
|   cur += len(address.String()) | ||||
| 
 | ||||
|   hmac := sha512.Sum512(append(packet[COMMAND_LENGTH+ID_LENGTH:cur], session_secret...)) | ||||
|   copy(packet[cur:], hmac[:]) | ||||
| 
 | ||||
|   return packet | ||||
| } | ||||
| 
 | ||||
| func ParseSessionConnect(session_connect []byte, session_secret []byte) (*net.UDPAddr, error) { | ||||
|   if len(session_connect) < SESSION_CONNECT_LENGTH { | ||||
|     return nil, fmt.Errorf("Bad session connect length: %d/%d", len(session_connect), SESSION_CONNECT_LENGTH) | ||||
|   } | ||||
| 
 | ||||
|   cur := 0 | ||||
| 
 | ||||
|   address_length := int(binary.BigEndian.Uint16(session_connect[cur:cur+2])) | ||||
|   cur += 2 | ||||
| 
 | ||||
|   if len(session_connect) != (SESSION_CONNECT_LENGTH + address_length) { | ||||
|     return nil, fmt.Errorf("Bad session connect length: %d/%d", len(session_connect), SESSION_CONNECT_LENGTH + address_length) | ||||
|   } | ||||
| 
 | ||||
|   address := string(session_connect[cur:cur+address_length]) | ||||
|   cur += address_length | ||||
| 
 | ||||
|   hmac_digest := make([]byte, cur) | ||||
|   copy(hmac_digest, session_connect[:cur]) | ||||
| 
 | ||||
|   hmac := session_connect[cur:cur+HMAC_LENGTH] | ||||
|   cur += HMAC_LENGTH | ||||
| 
 | ||||
|   calculated_hmac := sha512.Sum512(append(hmac_digest, session_secret...)) | ||||
|   if slices.Compare(hmac, calculated_hmac[:]) != 0 { | ||||
|     return nil, fmt.Errorf("Session connect bad HMAC") | ||||
|   } | ||||
| 
 | ||||
|   addr, err := net.ResolveUDPAddr("udp", address) | ||||
|   if err != nil { | ||||
|     return nil, fmt.Errorf("Error parsing return address: %w", err) | ||||
|   } | ||||
| 
 | ||||
|   return addr, nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionData(session *Session, packet []byte) ([]byte, error) { | ||||
|   iv := make([]byte, IV_LENGTH) | ||||
|   for i := 0; i < IV_LENGTH/8; i++ { | ||||
|     binary.BigEndian.PutUint64(iv[i*8:], session.iv_generator.Uint64()) | ||||
|   } | ||||
| 
 | ||||
|   stream := cipher.NewOFB(session.cipher, iv[:]) | ||||
|   header := make([]byte, COMMAND_LENGTH + ID_LENGTH + IV_LENGTH) | ||||
|   header[0] = byte(SESSION_DATA) | ||||
|   copy(header[COMMAND_LENGTH:], session.ID[:]) | ||||
|   copy(header[COMMAND_LENGTH+ID_LENGTH:], iv) | ||||
| 
 | ||||
|   packet_encrypted := bytes.NewBuffer(header) | ||||
|   writer := &cipher.StreamWriter{S: stream, W: packet_encrypted} | ||||
| 
 | ||||
|   // Encrypt the packet with a crc32 checksum appended to the end
 | ||||
|   _, err := io.Copy(writer, bytes.NewBuffer(binary.BigEndian.AppendUint32(packet, crc32.ChecksumIEEE(packet)))) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   } | ||||
| 
 | ||||
|   return packet_encrypted.Bytes(), nil | ||||
| } | ||||
| 
 | ||||
| func ParseSessionData(session *Session, encrypted []byte) ([]byte, error) { | ||||
|   iv := encrypted[0:IV_LENGTH] | ||||
| 
 | ||||
|   stream := cipher.NewOFB(session.cipher, iv) | ||||
|   var packet bytes.Buffer | ||||
|   reader := &cipher.StreamReader{S: stream, R: bytes.NewBuffer(encrypted[IV_LENGTH:])} | ||||
|   _, err := io.Copy(&packet, reader) | ||||
|   if err != nil { | ||||
|     return nil, err | ||||
|   } | ||||
| 
 | ||||
|   packet_clear := packet.Bytes() | ||||
|   checksum := binary.BigEndian.Uint32(packet_clear[len(packet_clear)-4:]) | ||||
|   data := packet_clear[:len(packet_clear)-4] | ||||
|   calculated := crc32.ChecksumIEEE(data) | ||||
|   if checksum != calculated { | ||||
|     return nil, fmt.Errorf("SESSION_DATA checksum mismatch: %x != %x", checksum, calculated) | ||||
|   } | ||||
| 
 | ||||
|   return data, nil | ||||
| } | ||||
| 
 | ||||
| func NewSessionClose(session *Session) []byte { | ||||
|   packet := make([]byte, COMMAND_LENGTH + ID_LENGTH + SESSION_CLOSE_LENGTH) | ||||
|   packet[0] = byte(SESSION_CLOSE) | ||||
|   copy(packet[1:], session.ID[:]) | ||||
| 
 | ||||
|   hmac := sha512.Sum512(append(session.ID[:], session.secret...)) | ||||
|   copy(packet[COMMAND_LENGTH + ID_LENGTH:], hmac[:]) | ||||
| 
 | ||||
|   return packet | ||||
| } | ||||
| 
 | ||||
| func ParseSessionClose(session *Session, hmac []byte) error { | ||||
|   calculated_hmac := sha512.Sum512(append(session.ID[:], session.secret...)) | ||||
|   if slices.Compare(hmac, calculated_hmac[:]) != 0 { | ||||
|     return fmt.Errorf("Session Close HMAC validation failed") | ||||
|   } else { | ||||
|     return nil | ||||
|   } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue