|
|
@ -12,6 +12,7 @@ import (
|
|
|
|
"io"
|
|
|
|
"io"
|
|
|
|
mrand "math/rand"
|
|
|
|
mrand "math/rand"
|
|
|
|
"net"
|
|
|
|
"net"
|
|
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"slices"
|
|
|
|
"slices"
|
|
|
@ -25,35 +26,43 @@ func(id SessionID) String() string {
|
|
|
|
return uuid.UUID(id).String()
|
|
|
|
return uuid.UUID(id).String()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type PeerID [32]byte
|
|
|
|
|
|
|
|
|
|
|
|
type Session struct {
|
|
|
|
type Session struct {
|
|
|
|
ID SessionID
|
|
|
|
ID SessionID
|
|
|
|
remote *net.UDPAddr
|
|
|
|
remote *net.UDPAddr
|
|
|
|
Peer PeerID
|
|
|
|
Peer PeerID
|
|
|
|
secret []byte
|
|
|
|
secret []byte
|
|
|
|
cipher cipher.Block
|
|
|
|
cipher cipher.Block
|
|
|
|
iv_generator mrand.Source64
|
|
|
|
iv_generator *mrand.Rand
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type SessionPacketType uint8
|
|
|
|
type SessionPacketType uint8
|
|
|
|
const (
|
|
|
|
const (
|
|
|
|
ID_LENGTH = 16
|
|
|
|
REQ_ID_LENGTH = 16
|
|
|
|
|
|
|
|
PEER_ID_LENGTH = 32
|
|
|
|
|
|
|
|
SESSION_ID_LENGTH = 16
|
|
|
|
IV_LENGTH = aes.BlockSize
|
|
|
|
IV_LENGTH = aes.BlockSize
|
|
|
|
PUBKEY_LENGTH = 32
|
|
|
|
PUBKEY_LENGTH = 32
|
|
|
|
ECDH_LENGTH = 32
|
|
|
|
|
|
|
|
SIGNATURE_LENGTH = 64
|
|
|
|
SIGNATURE_LENGTH = 64
|
|
|
|
HMAC_LENGTH = 64
|
|
|
|
HMAC_LENGTH = 64
|
|
|
|
COMMAND_LENGTH = 1
|
|
|
|
COMMAND_LENGTH = 1
|
|
|
|
|
|
|
|
TIME_LENGTH = 8
|
|
|
|
|
|
|
|
|
|
|
|
SESSION_OPEN_LENGTH = PUBKEY_LENGTH + ECDH_LENGTH + SIGNATURE_LENGTH
|
|
|
|
SESSION_OPEN_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SIGNATURE_LENGTH
|
|
|
|
SESSION_CONNECT_LENGTH = 2 + HMAC_LENGTH // + return addr string length
|
|
|
|
SESSION_OPENED_LENGTH = PUBKEY_LENGTH + PUBKEY_LENGTH + SESSION_ID_LENGTH + SIGNATURE_LENGTH
|
|
|
|
SESSION_CLOSE_LENGTH = HMAC_LENGTH
|
|
|
|
SESSION_TIMED_LENGTH = SESSION_ID_LENGTH + TIME_LENGTH + SIGNATURE_LENGTH
|
|
|
|
|
|
|
|
SESSION_TIMED_RESP_LENGTH = TIME_LENGTH + SIGNATURE_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
/*
|
|
|
|
pnyx session packets
|
|
|
|
pnyx session packets
|
|
|
|
*/
|
|
|
|
*/
|
|
|
|
SESSION_OPEN SessionPacketType = iota
|
|
|
|
SESSION_OPEN SessionPacketType = iota
|
|
|
|
|
|
|
|
SESSION_OPENED
|
|
|
|
SESSION_CONNECT
|
|
|
|
SESSION_CONNECT
|
|
|
|
|
|
|
|
SESSION_CONNECTED
|
|
|
|
SESSION_CLOSE
|
|
|
|
SESSION_CLOSE
|
|
|
|
|
|
|
|
SESSION_CLOSED
|
|
|
|
SESSION_DATA
|
|
|
|
SESSION_DATA
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
@ -104,9 +113,9 @@ func NewSessionOpen(key ed25519.PrivateKey) ([]byte, ed25519.PrivateKey, error)
|
|
|
|
return packet, private, nil
|
|
|
|
return packet, private, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ParseSessionOpen(ecdh ed25519.PrivateKey, session_open []byte) (Session, error) {
|
|
|
|
func ParseSessionOpen(key ed25519.PrivateKey, session_open []byte) (Session, []byte, error) {
|
|
|
|
if len(session_open) != SESSION_OPEN_LENGTH {
|
|
|
|
if len(session_open) != SESSION_OPEN_LENGTH {
|
|
|
|
return Session{}, fmt.Errorf("Bad SESSION_OPEN length: %d/%d", len(session_open), SESSION_OPEN_LENGTH)
|
|
|
|
return Session{}, nil, fmt.Errorf("Bad SESSION_OPEN length: %d/%d", len(session_open), SESSION_OPEN_LENGTH)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cur := 0
|
|
|
|
cur := 0
|
|
|
@ -122,20 +131,92 @@ func ParseSessionOpen(ecdh ed25519.PrivateKey, session_open []byte) (Session, er
|
|
|
|
cur += SIGNATURE_LENGTH
|
|
|
|
cur += SIGNATURE_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
if ed25519.Verify(client_pubkey, digest, signature) == false {
|
|
|
|
if ed25519.Verify(client_pubkey, digest, signature) == false {
|
|
|
|
return Session{}, fmt.Errorf("SESSION_OPEN signature verification failed")
|
|
|
|
return Session{}, nil, fmt.Errorf("SESSION_OPEN signature verification failed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
session_secret, err := ECDH(client_ecdh, ecdh)
|
|
|
|
seed_bytes := make([]byte, 8)
|
|
|
|
|
|
|
|
read, err := rand.Read(seed_bytes)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return Session{}, err
|
|
|
|
return Session{}, nil, err
|
|
|
|
|
|
|
|
} else if read != 8 {
|
|
|
|
|
|
|
|
return Session{}, nil, fmt.Errorf("Not enough entropy to create session")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
session_id := ID[SessionID](session_secret)
|
|
|
|
rand_gen := mrand.New(mrand.NewSource(int64(binary.BigEndian.Uint64(seed_bytes))).(mrand.Source64))
|
|
|
|
client_id := ID[PeerID](client_pubkey)
|
|
|
|
session_uuid, err := uuid.NewRandomFromReader(rand_gen)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return Session{}, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_id := SessionID(session_uuid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ecdh_public, ecdh_private, err := ed25519.GenerateKey(rand.Reader)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return Session{}, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_secret, err := ECDH(client_ecdh, ecdh_private)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return Session{}, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
session_cipher, err := aes.NewCipher(session_secret)
|
|
|
|
session_cipher, err := aes.NewCipher(session_secret)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
return Session{}, err
|
|
|
|
return Session{}, nil, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_opened := make([]byte, COMMAND_LENGTH + SESSION_OPENED_LENGTH)
|
|
|
|
|
|
|
|
cur = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_opened[cur] = byte(SESSION_OPENED)
|
|
|
|
|
|
|
|
cur += COMMAND_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
copy(session_opened[cur:], key.Public().(ed25519.PublicKey))
|
|
|
|
|
|
|
|
cur += PUBKEY_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
copy(session_opened[cur:], ecdh_public)
|
|
|
|
|
|
|
|
cur += PUBKEY_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
copy(session_opened[cur:], session_id[:])
|
|
|
|
|
|
|
|
cur += SESSION_ID_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
signature = ed25519.Sign(key, session_opened[COMMAND_LENGTH:cur])
|
|
|
|
|
|
|
|
copy(session_opened[cur:], signature)
|
|
|
|
|
|
|
|
cur += SIGNATURE_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return Session{
|
|
|
|
|
|
|
|
ID: session_id,
|
|
|
|
|
|
|
|
remote: nil,
|
|
|
|
|
|
|
|
Peer: PeerID(client_pubkey),
|
|
|
|
|
|
|
|
iv_generator: rand_gen,
|
|
|
|
|
|
|
|
cipher: session_cipher,
|
|
|
|
|
|
|
|
secret: session_secret,
|
|
|
|
|
|
|
|
}, session_opened, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func ParseSessionOpened(expected_pubkey ed25519.PublicKey, ecdh_private ed25519.PrivateKey, session_opened []byte) (Session, error) {
|
|
|
|
|
|
|
|
if len(session_opened) != SESSION_OPENED_LENGTH {
|
|
|
|
|
|
|
|
return Session{}, fmt.Errorf("Wrong SESSION_OPENED length: %d/%d", len(session_opened), SESSION_OPEN_LENGTH)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cur := 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server_pubkey := (ed25519.PublicKey)(session_opened[cur:cur+PUBKEY_LENGTH])
|
|
|
|
|
|
|
|
cur += PUBKEY_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server_ecdh := (ed25519.PublicKey)(session_opened[cur:cur+PUBKEY_LENGTH])
|
|
|
|
|
|
|
|
cur += PUBKEY_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_id := SessionID(session_opened[cur:cur+SESSION_ID_LENGTH])
|
|
|
|
|
|
|
|
cur += SESSION_ID_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if expected_pubkey != nil && slices.Compare(server_pubkey, expected_pubkey) != 0 {
|
|
|
|
|
|
|
|
return Session{}, fmt.Errorf("server public key %x does not match expected %x", server_pubkey, expected_pubkey)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ed25519.Verify(server_pubkey, session_opened[:cur], session_opened[cur:cur+SIGNATURE_LENGTH]) == false {
|
|
|
|
|
|
|
|
return Session{}, fmt.Errorf("session opened signature verification failed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
seed_bytes := make([]byte, 8)
|
|
|
|
seed_bytes := make([]byte, 8)
|
|
|
@ -146,73 +227,77 @@ func ParseSessionOpen(ecdh ed25519.PrivateKey, session_open []byte) (Session, er
|
|
|
|
return Session{}, fmt.Errorf("Not enough entropy to create session")
|
|
|
|
return Session{}, fmt.Errorf("Not enough entropy to create session")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
session_secret, err := ECDH(server_ecdh, ecdh_private)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return Session{}, err
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cipher, err := aes.NewCipher(session_secret)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
return Session{}, nil
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Session{
|
|
|
|
return Session{
|
|
|
|
ID: session_id,
|
|
|
|
ID: session_id,
|
|
|
|
remote: nil,
|
|
|
|
remote: nil,
|
|
|
|
Peer: client_id,
|
|
|
|
Peer: PeerID(server_pubkey),
|
|
|
|
secret: session_secret,
|
|
|
|
secret: session_secret,
|
|
|
|
cipher: session_cipher,
|
|
|
|
cipher: cipher,
|
|
|
|
iv_generator: mrand.NewSource(int64(binary.BigEndian.Uint64(seed_bytes))).(mrand.Source64),
|
|
|
|
iv_generator: mrand.New(mrand.NewSource(int64(binary.BigEndian.Uint64(seed_bytes))).(mrand.Source64)),
|
|
|
|
}, nil
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewSessionConnect(address *net.UDPAddr, session_secret []byte) []byte {
|
|
|
|
func NewSessionTimed(command SessionPacketType, key ed25519.PrivateKey, session *Session, t time.Time) []byte {
|
|
|
|
packet := make([]byte, COMMAND_LENGTH + ID_LENGTH + SESSION_CONNECT_LENGTH + len(address.String()))
|
|
|
|
packet := make([]byte, COMMAND_LENGTH + SESSION_TIMED_LENGTH)
|
|
|
|
cur := 0
|
|
|
|
cur := 0
|
|
|
|
|
|
|
|
|
|
|
|
packet[cur] = byte(SESSION_CONNECT)
|
|
|
|
packet[cur] = byte(command)
|
|
|
|
cur += COMMAND_LENGTH
|
|
|
|
cur += COMMAND_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
session_id := [16]byte(ID[SessionID](session_secret))
|
|
|
|
copy(packet[cur:], session.ID[:])
|
|
|
|
copy(packet[cur:], session_id[:])
|
|
|
|
cur += SESSION_ID_LENGTH
|
|
|
|
cur += ID_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
binary.BigEndian.PutUint16(packet[cur:], uint16(len(address.String())))
|
|
|
|
binary.BigEndian.PutUint64(packet[cur:], uint64(t.UnixMilli()))
|
|
|
|
cur += 2
|
|
|
|
cur += TIME_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
copy(packet[cur:], []byte(address.String()))
|
|
|
|
signature := ed25519.Sign(key, packet[COMMAND_LENGTH:cur])
|
|
|
|
cur += len(address.String())
|
|
|
|
copy(packet[cur:], signature)
|
|
|
|
|
|
|
|
cur += SIGNATURE_LENGTH
|
|
|
|
hmac := sha512.Sum512(append(packet[COMMAND_LENGTH+ID_LENGTH:cur], session_secret...))
|
|
|
|
|
|
|
|
copy(packet[cur:], hmac[:])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return packet
|
|
|
|
return packet
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ParseSessionConnect(session_connect []byte, session_secret []byte) (*net.UDPAddr, error) {
|
|
|
|
func ParseSessionTimed(resp_type SessionPacketType, key ed25519.PrivateKey, packet []byte, session *Session, last_t int64) ([]byte, error) {
|
|
|
|
if len(session_connect) < SESSION_CONNECT_LENGTH {
|
|
|
|
if len(packet) != SESSION_TIMED_LENGTH {
|
|
|
|
return nil, fmt.Errorf("Bad session connect length: %d/%d", len(session_connect), SESSION_CONNECT_LENGTH)
|
|
|
|
return nil, fmt.Errorf("Bad timed packet length: %d/%d", len(packet), SESSION_TIMED_LENGTH)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cur := 0
|
|
|
|
cur := SESSION_ID_LENGTH
|
|
|
|
|
|
|
|
t := int64(binary.BigEndian.Uint64(packet[cur:]))
|
|
|
|
|
|
|
|
cur += TIME_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
address_length := int(binary.BigEndian.Uint16(session_connect[cur:cur+2]))
|
|
|
|
if t < last_t {
|
|
|
|
cur += 2
|
|
|
|
return nil, fmt.Errorf("Time in packet to old: %d < %d", t, last_t)
|
|
|
|
|
|
|
|
|
|
|
|
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])
|
|
|
|
if ed25519.Verify(ed25519.PublicKey(session.Peer[:]), packet[:cur], packet[cur:cur+SIGNATURE_LENGTH]) == false {
|
|
|
|
cur += address_length
|
|
|
|
return nil, fmt.Errorf("Failed to verify packet signature")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
hmac_digest := make([]byte, cur)
|
|
|
|
resp := make([]byte, COMMAND_LENGTH + SESSION_TIMED_RESP_LENGTH)
|
|
|
|
copy(hmac_digest, session_connect[:cur])
|
|
|
|
cur = 0
|
|
|
|
|
|
|
|
|
|
|
|
hmac := session_connect[cur:cur+HMAC_LENGTH]
|
|
|
|
resp[cur] = byte(resp_type)
|
|
|
|
cur += HMAC_LENGTH
|
|
|
|
cur += COMMAND_LENGTH
|
|
|
|
|
|
|
|
|
|
|
|
calculated_hmac := sha512.Sum512(append(hmac_digest, session_secret...))
|
|
|
|
binary.BigEndian.PutUint64(resp[cur:], uint64(t))
|
|
|
|
if slices.Compare(hmac, calculated_hmac[:]) != 0 {
|
|
|
|
cur += TIME_LENGTH
|
|
|
|
return nil, fmt.Errorf("Session connect bad HMAC")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addr, err := net.ResolveUDPAddr("udp", address)
|
|
|
|
signature := ed25519.Sign(key, resp[COMMAND_LENGTH:cur])
|
|
|
|
if err != nil {
|
|
|
|
copy(resp[cur:], signature)
|
|
|
|
return nil, fmt.Errorf("Error parsing return address: %w", err)
|
|
|
|
cur += SIGNATURE_LENGTH
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return addr, nil
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewSessionData(session *Session, packet []byte) ([]byte, error) {
|
|
|
|
func NewSessionData(session *Session, packet []byte) ([]byte, error) {
|
|
|
@ -222,10 +307,10 @@ func NewSessionData(session *Session, packet []byte) ([]byte, error) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stream := cipher.NewOFB(session.cipher, iv[:])
|
|
|
|
stream := cipher.NewOFB(session.cipher, iv[:])
|
|
|
|
header := make([]byte, COMMAND_LENGTH + ID_LENGTH + IV_LENGTH)
|
|
|
|
header := make([]byte, COMMAND_LENGTH + SESSION_ID_LENGTH + IV_LENGTH)
|
|
|
|
header[0] = byte(SESSION_DATA)
|
|
|
|
header[0] = byte(SESSION_DATA)
|
|
|
|
copy(header[COMMAND_LENGTH:], session.ID[:])
|
|
|
|
copy(header[COMMAND_LENGTH:], session.ID[:])
|
|
|
|
copy(header[COMMAND_LENGTH+ID_LENGTH:], iv)
|
|
|
|
copy(header[COMMAND_LENGTH+SESSION_ID_LENGTH:], iv)
|
|
|
|
|
|
|
|
|
|
|
|
packet_encrypted := bytes.NewBuffer(header)
|
|
|
|
packet_encrypted := bytes.NewBuffer(header)
|
|
|
|
writer := &cipher.StreamWriter{S: stream, W: packet_encrypted}
|
|
|
|
writer := &cipher.StreamWriter{S: stream, W: packet_encrypted}
|
|
|
@ -260,23 +345,3 @@ func ParseSessionData(session *Session, encrypted []byte) ([]byte, error) {
|
|
|
|
|
|
|
|
|
|
|
|
return data, nil
|
|
|
|
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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|