You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
obfs4/transports/scramblesuit/handshake_ticket.go

229 lines
5.7 KiB
Go

/*
* Copyright (c) 2015, Yawning Angel <yawning at schwanenlied dot me>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package scramblesuit
import (
"bytes"
"encoding/base32"
"encoding/json"
"errors"
"fmt"
"hash"
"io/ioutil"
"net"
"os"
"path"
"strconv"
"sync"
"time"
"gitlab.com/yawning/obfs4.git/common/csrand"
)
const (
ticketFile = "scramblesuit_tickets.json"
ticketKeyLength = 32
ticketLength = 112
ticketLifetime = 60 * 60 * 24 * 7
ticketMinPadLength = 0
ticketMaxPadLength = 1388
)
var (
errInvalidTicket = errors.New("scramblesuit: invalid serialized ticket")
)
type ssTicketStore struct {
sync.Mutex
filePath string
store map[string]*ssTicket
}
type ssTicket struct {
key [ticketKeyLength]byte
ticket [ticketLength]byte
issuedAt int64
}
type ssTicketJSON struct {
KeyTicket string `json:"key-ticket"`
IssuedAt int64 `json:"issuedAt"`
}
func (t *ssTicket) isValid() bool {
return t.issuedAt+ticketLifetime > time.Now().Unix()
}
func newTicket(raw []byte) (*ssTicket, error) {
if len(raw) != ticketKeyLength+ticketLength {
return nil, errInvalidTicket
}
t := &ssTicket{issuedAt: time.Now().Unix()}
copy(t.key[:], raw[0:])
copy(t.ticket[:], raw[ticketKeyLength:])
return t, nil
}
func (s *ssTicketStore) storeTicket(addr net.Addr, rawT []byte) {
t, err := newTicket(rawT)
if err != nil {
// Silently ignore ticket store failures.
return
}
s.Lock()
defer s.Unlock()
// Add the ticket to the map, and checkpoint to disk. Serialization errors
// are ignored because the handshake code will just use UniformDH if a
// ticket is not available.
s.store[addr.String()] = t
_ = s.serialize()
}
func (s *ssTicketStore) getTicket(addr net.Addr) (*ssTicket, error) {
aStr := addr.String()
s.Lock()
defer s.Unlock()
t, ok := s.store[aStr]
if ok && t != nil {
// Tickets are one use only, so remove tickets from the map, and
// checkpoint the map to disk.
delete(s.store, aStr)
err := s.serialize()
if !t.isValid() {
// Expired ticket, ignore it.
return nil, err
}
return t, err
}
// No ticket was found, that's fine.
return nil, nil
}
func (s *ssTicketStore) serialize() error {
encMap := make(map[string]*ssTicketJSON)
for k, v := range s.store {
kt := make([]byte, 0, ticketKeyLength+ticketLength)
kt = append(kt, v.key[:]...)
kt = append(kt, v.ticket[:]...)
ktStr := base32.StdEncoding.EncodeToString(kt)
jsonObj := &ssTicketJSON{KeyTicket: ktStr, IssuedAt: v.issuedAt}
encMap[k] = jsonObj
}
jsonStr, err := json.Marshal(encMap)
if err != nil {
return err
}
return ioutil.WriteFile(s.filePath, jsonStr, 0600)
}
func loadTicketStore(stateDir string) (*ssTicketStore, error) {
fPath := path.Join(stateDir, ticketFile)
s := &ssTicketStore{filePath: fPath}
s.store = make(map[string]*ssTicket)
f, err := ioutil.ReadFile(fPath)
if err != nil {
// No ticket store is fine.
if os.IsNotExist(err) {
return s, nil
}
// But a file read error is not.
return nil, err
}
encMap := make(map[string]*ssTicketJSON)
if err = json.Unmarshal(f, &encMap); err != nil {
return nil, fmt.Errorf("failed to load ticket store '%s': '%s'", fPath, err)
}
for k, v := range encMap {
raw, err := base32.StdEncoding.DecodeString(v.KeyTicket)
if err != nil || len(raw) != ticketKeyLength+ticketLength {
// Just silently skip corrupted tickets.
continue
}
t := &ssTicket{issuedAt: v.IssuedAt}
if !t.isValid() {
// Just ignore expired tickets.
continue
}
copy(t.key[:], raw[0:])
copy(t.ticket[:], raw[ticketKeyLength:])
s.store[k] = t
}
return s, nil
}
type ssTicketClientHandshake struct {
mac hash.Hash
ticket *ssTicket
padLen int
}
func (hs *ssTicketClientHandshake) generateHandshake() ([]byte, error) {
var buf bytes.Buffer
hs.mac.Reset()
// The client handshake is T | P | M | MAC(T | P | M | E)
_, _ = hs.mac.Write(hs.ticket.ticket[:])
m := hs.mac.Sum(nil)[:macLength]
p, err := makePad(hs.padLen)
if err != nil {
return nil, err
}
// Write T, P, M.
buf.Write(hs.ticket.ticket[:])
buf.Write(p)
buf.Write(m)
// Calculate and write the MAC.
e := []byte(strconv.FormatInt(getEpochHour(), 10))
_, _ = hs.mac.Write(p)
_, _ = hs.mac.Write(m)
_, _ = hs.mac.Write(e)
buf.Write(hs.mac.Sum(nil)[:macLength])
hs.mac.Reset()
return buf.Bytes(), nil
}
func newTicketClientHandshake(mac hash.Hash, ticket *ssTicket) *ssTicketClientHandshake {
hs := &ssTicketClientHandshake{mac: mac, ticket: ticket}
hs.padLen = csrand.IntRange(ticketMinPadLength, ticketMaxPadLength)
return hs
}