mirror of https://gitlab.com/yawning/obfs4
Add support for acting as a ScrambleSuit client.
This allows obfs4proxy to be used as a ScrambleSuit client that is wire compatible with the obfs4proxy implementation, including session ticket support, and length obfuscation. The current implementation has the following limitations: * IAT obfuscation is not supported (and is disabled in all other ScrambleSuit implementations by default). * The length distribution and probabilites are different from those generated by obfsproxy and obfsclient due to a different DRBG. * Server support is missing and is unlikely to be implemented.merge-requests/3/head
parent
0f038ca4fa
commit
0066cfc393
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* 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 provides an implementation of the ScrambleSuit
|
||||
// obfuscation protocol. The implementation is client only.
|
||||
package scramblesuit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"git.torproject.org/pluggable-transports/goptlib.git"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/transports/base"
|
||||
)
|
||||
|
||||
const transportName = "scramblesuit"
|
||||
|
||||
// Transport is the ScrambleSuit implementation of the base.Transport interface.
|
||||
type Transport struct{}
|
||||
|
||||
// Name returns the name of the ScrambleSuit transport protocol.
|
||||
func (t *Transport) Name() string {
|
||||
return transportName
|
||||
}
|
||||
|
||||
// ClientFactory returns a new ssClientFactory instance.
|
||||
func (t *Transport) ClientFactory(stateDir string) (base.ClientFactory, error) {
|
||||
tStore, err := loadTicketStore(stateDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cf := &ssClientFactory{transport: t, ticketStore: tStore}
|
||||
return cf, nil
|
||||
}
|
||||
|
||||
// ServerFactory will one day return a new ssServerFactory instance.
|
||||
func (t *Transport) ServerFactory(stateDir string, args *pt.Args) (base.ServerFactory, error) {
|
||||
// TODO: Fill this in eventually, though obfs4 is better.
|
||||
return nil, fmt.Errorf("server not supported")
|
||||
}
|
||||
|
||||
type ssClientFactory struct {
|
||||
transport base.Transport
|
||||
ticketStore *ssTicketStore
|
||||
}
|
||||
|
||||
func (cf *ssClientFactory) Transport() base.Transport {
|
||||
return cf.transport
|
||||
}
|
||||
|
||||
func (cf *ssClientFactory) ParseArgs(args *pt.Args) (interface{}, error) {
|
||||
return newClientArgs(args)
|
||||
}
|
||||
|
||||
func (cf *ssClientFactory) WrapConn(conn net.Conn, args interface{}) (net.Conn, error) {
|
||||
ca, ok := args.(*ssClientArgs)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid argument type for args")
|
||||
}
|
||||
return newScrambleSuitClientConn(conn, cf.ticketStore, ca)
|
||||
}
|
||||
|
||||
var _ base.ClientFactory = (*ssClientFactory)(nil)
|
||||
var _ base.Transport = (*Transport)(nil)
|
@ -0,0 +1,521 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* 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"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"git.torproject.org/pluggable-transports/goptlib.git"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/drbg"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/probdist"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
|
||||
)
|
||||
|
||||
const (
|
||||
passwordArg = "password"
|
||||
|
||||
maxSegmentLength = 1448
|
||||
maxPayloadLength = 1427
|
||||
sharedSecretLength = 160 / 8 // k_B
|
||||
clientHandshakeTimeout = time.Duration(60) * time.Second
|
||||
|
||||
minLenDistLength = 21
|
||||
maxLenDistLength = maxSegmentLength
|
||||
|
||||
keyLength = 32 + 8 + 32
|
||||
|
||||
pktPrngSeedLength = 32
|
||||
pktOverhead = macLength + pktHdrLength
|
||||
pktHdrLength = 2 + 2 + 1
|
||||
pktPayload = 1
|
||||
pktNewTicket = 1 << 1
|
||||
pktPrngSeed = 1 << 2
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotSupported is the error returned for a unsupported operation.
|
||||
ErrNotSupported = errors.New("scramblesuit: operation not supported")
|
||||
|
||||
// ErrInvalidPacket is the error returned when a invalid packet is received.
|
||||
ErrInvalidPacket = errors.New("scramblesuit: invalid packet")
|
||||
|
||||
zeroPadBytes [maxPayloadLength]byte
|
||||
)
|
||||
|
||||
type ssSharedSecret [sharedSecretLength]byte
|
||||
|
||||
type ssClientArgs struct {
|
||||
kB *ssSharedSecret
|
||||
sessionKey *uniformdh.PrivateKey
|
||||
}
|
||||
|
||||
func newClientArgs(args *pt.Args) (ca *ssClientArgs, err error) {
|
||||
ca = &ssClientArgs{}
|
||||
if ca.kB, err = parsePasswordArg(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Generate the client keypair before opening a connection since the time
|
||||
// taken is visible to an adversary. This key might not end up being used
|
||||
// if a session ticket is present, but this doesn't take that long.
|
||||
if ca.sessionKey, err = uniformdh.GenerateKey(csrand.Reader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func parsePasswordArg(args *pt.Args) (*ssSharedSecret, error) {
|
||||
str, ok := args.Get(passwordArg)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("missing argument '%s'", passwordArg)
|
||||
}
|
||||
|
||||
// To match the obfsproxy behavior, 'str' should contain a Base32 encoded
|
||||
// shared secret (k_B) used for handshaking.
|
||||
decoded, err := base32.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode password: %s", err)
|
||||
}
|
||||
if len(decoded) != sharedSecretLength {
|
||||
return nil, fmt.Errorf("password length %d is invalid", len(decoded))
|
||||
}
|
||||
ss := new(ssSharedSecret)
|
||||
copy(ss[:], decoded)
|
||||
return ss, nil
|
||||
}
|
||||
|
||||
type ssCryptoState struct {
|
||||
s cipher.Stream
|
||||
mac hash.Hash
|
||||
}
|
||||
|
||||
func newCryptoState(aesKey []byte, ivPrefix []byte, macKey []byte) (*ssCryptoState, error) {
|
||||
// The ScrambleSuit CTR-AES256 link crypto uses an 8 byte prefix from the
|
||||
// KDF, and a 64 bit counter initialized to 1 as the IV. The initial value
|
||||
// of the counter isn't documented in the spec either.
|
||||
var initialCtr = []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
iv := make([]byte, 0, aes.BlockSize)
|
||||
iv = append(iv, ivPrefix...)
|
||||
iv = append(iv, initialCtr...)
|
||||
b, err := aes.NewCipher(aesKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := cipher.NewCTR(b, iv)
|
||||
mac := hmac.New(sha256.New, macKey)
|
||||
return &ssCryptoState{s: s, mac: mac}, nil
|
||||
}
|
||||
|
||||
type ssConn struct {
|
||||
net.Conn
|
||||
|
||||
isServer bool
|
||||
|
||||
lenDist *probdist.WeightedDist
|
||||
receiveBuffer *bytes.Buffer
|
||||
receiveDecodedBuffer *bytes.Buffer
|
||||
receiveState ssRxState
|
||||
|
||||
txCrypto *ssCryptoState
|
||||
rxCrypto *ssCryptoState
|
||||
|
||||
ticketStore *ssTicketStore
|
||||
}
|
||||
|
||||
type ssRxState struct {
|
||||
mac []byte
|
||||
hdr []byte
|
||||
|
||||
totalLen int
|
||||
payloadLen int
|
||||
}
|
||||
|
||||
func (conn *ssConn) Read(b []byte) (n int, err error) {
|
||||
// If the receive payload buffer is empty, consume data off the network.
|
||||
for conn.receiveDecodedBuffer.Len() == 0 {
|
||||
if err = conn.readPackets(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Service the read request using buffered payload.
|
||||
if conn.receiveDecodedBuffer.Len() > 0 {
|
||||
n, _ = conn.receiveDecodedBuffer.Read(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *ssConn) Write(b []byte) (n int, err error) {
|
||||
var frameBuf bytes.Buffer
|
||||
p := b
|
||||
toSend := len(p)
|
||||
|
||||
for toSend > 0 {
|
||||
// Send as much payload as will fit into each frame as possible.
|
||||
wrLen := len(p)
|
||||
if wrLen > maxPayloadLength {
|
||||
wrLen = maxPayloadLength
|
||||
}
|
||||
payload := p[:wrLen]
|
||||
if err = conn.makePacket(&frameBuf, pktPayload, payload, 0); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
toSend -= wrLen
|
||||
p = p[wrLen:]
|
||||
n += wrLen
|
||||
}
|
||||
|
||||
// Pad out the burst as appropriate.
|
||||
if err = conn.padBurst(&frameBuf, conn.lenDist.Sample()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Write and return.
|
||||
_, err = conn.Conn.Write(frameBuf.Bytes())
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *ssConn) SetDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (conn *ssConn) SetReadDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (conn *ssConn) SetWriteDeadline(t time.Time) error {
|
||||
return ErrNotSupported
|
||||
}
|
||||
|
||||
func (conn *ssConn) makePacket(w io.Writer, pktType byte, data []byte, padLen int) error {
|
||||
payloadLen := len(data)
|
||||
totalLen := payloadLen + padLen
|
||||
if totalLen > maxPayloadLength {
|
||||
panic(fmt.Sprintf("BUG: makePacket() len(data) + padLen > maxPayloadLength: %d + %d > %d", len(data), padLen, maxPayloadLength))
|
||||
}
|
||||
|
||||
// Build the packet header (total length, payload length, flags),
|
||||
// and append the payload and padding.
|
||||
pkt := make([]byte, pktHdrLength, pktHdrLength+payloadLen+padLen)
|
||||
binary.BigEndian.PutUint16(pkt[0:], uint16(totalLen))
|
||||
binary.BigEndian.PutUint16(pkt[2:], uint16(payloadLen))
|
||||
pkt[4] = pktType
|
||||
pkt = append(pkt, data...)
|
||||
pkt = append(pkt, zeroPadBytes[:padLen]...)
|
||||
|
||||
// Encrypt the packet, and calculate the MAC.
|
||||
conn.txCrypto.s.XORKeyStream(pkt, pkt)
|
||||
conn.txCrypto.mac.Reset()
|
||||
conn.txCrypto.mac.Write(pkt)
|
||||
mac := conn.txCrypto.mac.Sum(nil)[:macLength]
|
||||
|
||||
// Write out MAC | Packet. Note that this does not go onto the network
|
||||
// yet, as w is a byte.Buffer (This is done so each call to conn.Write()
|
||||
// gets padding added).
|
||||
if _, err := w.Write(mac); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(pkt)
|
||||
return err
|
||||
}
|
||||
|
||||
func (conn *ssConn) readPackets() error {
|
||||
// Consume and buffer up to 1 MSS worth of data.
|
||||
var buf [maxSegmentLength]byte
|
||||
rdLen, rdErr := conn.Conn.Read(buf[:])
|
||||
conn.receiveBuffer.Write(buf[:rdLen])
|
||||
|
||||
// Process incoming packets incrementally. conn.receiveState stores
|
||||
// the results of partial processing.
|
||||
for conn.receiveBuffer.Len() > 0 {
|
||||
if conn.receiveState.mac == nil {
|
||||
// Read and store the packet MAC.
|
||||
if conn.receiveBuffer.Len() < macLength {
|
||||
break
|
||||
}
|
||||
mac := make([]byte, macLength)
|
||||
conn.receiveBuffer.Read(mac)
|
||||
conn.receiveState.mac = mac
|
||||
}
|
||||
|
||||
if conn.receiveState.hdr == nil {
|
||||
// Read and store the packet header.
|
||||
if conn.receiveBuffer.Len() < pktHdrLength {
|
||||
break
|
||||
}
|
||||
hdr := make([]byte, pktHdrLength)
|
||||
conn.receiveBuffer.Read(hdr)
|
||||
|
||||
// Add the encrypted packet header to the HMAC instance, and then
|
||||
// decrypt it so that the length of the packet can be determined.
|
||||
conn.rxCrypto.mac.Reset()
|
||||
conn.rxCrypto.mac.Write(hdr)
|
||||
conn.rxCrypto.s.XORKeyStream(hdr, hdr)
|
||||
|
||||
// Store the plaintext packet header, and host byte order length
|
||||
// values.
|
||||
totalLen := int(binary.BigEndian.Uint16(hdr[0:]))
|
||||
payloadLen := int(binary.BigEndian.Uint16(hdr[2:]))
|
||||
if payloadLen > totalLen || totalLen > maxPayloadLength {
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
conn.receiveState.hdr = hdr
|
||||
conn.receiveState.totalLen = totalLen
|
||||
conn.receiveState.payloadLen = payloadLen
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if conn.receiveState.totalLen > 0 {
|
||||
// If the packet actually has payload (including padding), read,
|
||||
// digest and decrypt it.
|
||||
if conn.receiveBuffer.Len() < conn.receiveState.totalLen {
|
||||
break
|
||||
}
|
||||
data = make([]byte, conn.receiveState.totalLen)
|
||||
conn.receiveBuffer.Read(data)
|
||||
conn.rxCrypto.mac.Write(data)
|
||||
conn.rxCrypto.s.XORKeyStream(data, data)
|
||||
}
|
||||
|
||||
// Authenticate the packet, by comparing the received MAC with the one
|
||||
// calculated over the ciphertext consumed off the network.
|
||||
cmpMAC := conn.rxCrypto.mac.Sum(nil)[:macLength]
|
||||
if !hmac.Equal(cmpMAC, conn.receiveState.mac[:]) {
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
|
||||
// Based on the packet flags, do something useful with the payload.
|
||||
data = data[:conn.receiveState.payloadLen]
|
||||
switch conn.receiveState.hdr[4] {
|
||||
case pktPayload:
|
||||
// User data, write it into the decoded payload buffer so that Read
|
||||
// calls can be serviced.
|
||||
conn.receiveDecodedBuffer.Write(data)
|
||||
case pktNewTicket:
|
||||
// New Session Ticket to be used for future handshakes, store it in
|
||||
// the Session Ticket store.
|
||||
if conn.isServer || len(data) != ticketKeyLength+ticketLength {
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
conn.ticketStore.storeTicket(conn.RemoteAddr(), data)
|
||||
case pktPrngSeed:
|
||||
// New PRNG_SEED for the protocol polymorphism. Regenerate the
|
||||
// length obfuscation probability distribution.
|
||||
if conn.isServer || len(data) != pktPrngSeedLength {
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
seed, err := drbg.SeedFromBytes(data)
|
||||
if err != nil {
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
conn.lenDist.Reset(seed)
|
||||
default:
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
|
||||
// Done processing a packet, clear the partial state.
|
||||
conn.receiveState.mac = nil
|
||||
conn.receiveState.hdr = nil
|
||||
conn.receiveState.totalLen = 0
|
||||
conn.receiveState.payloadLen = 0
|
||||
}
|
||||
return rdErr
|
||||
}
|
||||
|
||||
func (conn *ssConn) clientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) error {
|
||||
if conn.isServer {
|
||||
return fmt.Errorf("clientHandshake called on server connection")
|
||||
}
|
||||
|
||||
// Query the Session Ticket store to see if there is a stored session
|
||||
// ticket.
|
||||
ticket, err := conn.ticketStore.getTicket(conn.RemoteAddr())
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ticket != nil {
|
||||
// Ok, there is an existing ticket, so attempt to do a Session Ticket
|
||||
// handshake. Until we write to the network, failures are non-fatal as
|
||||
// we can transition gracefully into doing a UniformDH handshake.
|
||||
|
||||
// Derive the keys from the prestored master key received with the
|
||||
// ticket. This is done before the actual handshake since the
|
||||
// handshake uses the outgoing HMAC-SHA256-128 key for authentication.
|
||||
if err = conn.initCrypto(ticket.key[:]); err != nil {
|
||||
goto handshakeUDH
|
||||
}
|
||||
|
||||
// Generate and send the ticket handshake. There is no response, since
|
||||
// both sides have the keying material.
|
||||
hs := newTicketClientHandshake(conn.txCrypto.mac, ticket)
|
||||
blob, err := hs.generateHandshake()
|
||||
if err != nil {
|
||||
goto handshakeUDH
|
||||
}
|
||||
if _, err = conn.Conn.Write(blob); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
handshakeUDH:
|
||||
// No session ticket, so take the slow path and do a UniformDH based
|
||||
// handshake.
|
||||
|
||||
// Generate and send the client handshake.
|
||||
hs := newDHClientHandshake(kB, sessionKey)
|
||||
blob, err := hs.generateHandshake()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = conn.Conn.Write(blob); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Consume the server handshake. Since we don't actually know the length
|
||||
// of the respose, we need to consume data off the network till we either
|
||||
// find the tail marker + MAC digest indicating that a handshake response
|
||||
// has been received, or the maximum handshake size passes without a valid
|
||||
// response.
|
||||
var hsBuf [maxHandshakeLength]byte
|
||||
for {
|
||||
var n int
|
||||
if n, err = conn.Conn.Read(hsBuf[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
conn.receiveBuffer.Write(hsBuf[:n])
|
||||
|
||||
// Attempt to process all the data seen so far as a response.
|
||||
var seed []byte
|
||||
n, seed, err = hs.parseServerHandshake(conn.receiveBuffer.Bytes())
|
||||
if err == errMarkNotFoundYet {
|
||||
// No response found yet, keep trying.
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ok, done processing the handshake, discard the response, and do the
|
||||
// key derivation based off the calculated shared secret.
|
||||
_ = conn.receiveBuffer.Next(n)
|
||||
err = conn.initCrypto(seed)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *ssConn) initCrypto(seed []byte) (err error) {
|
||||
// Use HKDF-SHA256 (Expand only, no Extract) to generate session keys from
|
||||
// initial keying material.
|
||||
okm := hkdfExpand(sha256.New, seed, nil, kdfSecretLength)
|
||||
if conn.txCrypto, err = newCryptoState(okm[0:32], okm[32:40], okm[80:112]); err != nil {
|
||||
return
|
||||
}
|
||||
if conn.rxCrypto, err = newCryptoState(okm[40:72], okm[72:80], okm[112:144]); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *ssConn) padBurst(burst *bytes.Buffer, sampleLen int) (err error) {
|
||||
// Burst contains the fully encrypted+MACed outgoing payload that will be
|
||||
// written to the network. Pad it out so that the last segment (based on
|
||||
// the ScrambleSuit MTU) is sampleLen bytes.
|
||||
|
||||
dataLen := burst.Len() % maxSegmentLength
|
||||
padLen := 0
|
||||
if sampleLen >= dataLen {
|
||||
padLen = sampleLen - dataLen
|
||||
} else {
|
||||
padLen = (maxSegmentLength - dataLen) + sampleLen
|
||||
}
|
||||
if padLen < pktOverhead {
|
||||
// The padLen is less than the MAC + packet header in length, so
|
||||
// two packets are required.
|
||||
padLen += maxSegmentLength
|
||||
}
|
||||
|
||||
if padLen == 0 {
|
||||
return
|
||||
} else if padLen > maxSegmentLength {
|
||||
// Note: packetmorpher.py: getPadding is slightly wrong and only
|
||||
// accounts for one of the two packet headers.
|
||||
if err = conn.makePacket(burst, pktPayload, nil, 700-pktOverhead); err != nil {
|
||||
return
|
||||
}
|
||||
err = conn.makePacket(burst, pktPayload, nil, padLen-(700+2*pktOverhead))
|
||||
} else {
|
||||
err = conn.makePacket(burst, pktPayload, nil, padLen-pktOverhead)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newScrambleSuitClientConn(conn net.Conn, tStore *ssTicketStore, ca *ssClientArgs) (net.Conn, error) {
|
||||
// At this point we have kB and our session key, so we can directly
|
||||
// start handshaking and seeing what happens.
|
||||
|
||||
// Seed the initial polymorphism distribution.
|
||||
seed, err := drbg.NewSeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dist := probdist.New(seed, minLenDistLength, maxLenDistLength, true)
|
||||
|
||||
// Allocate the client structure.
|
||||
c := &ssConn{conn, false, dist, bytes.NewBuffer(nil), bytes.NewBuffer(nil), ssRxState{}, nil, nil, tStore}
|
||||
|
||||
// Start the handshake timeout.
|
||||
deadline := time.Now().Add(clientHandshakeTimeout)
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Attempt to handshake.
|
||||
if err := c.clientHandshake(ca.kB, ca.sessionKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stop the handshake timeout.
|
||||
if err := conn.SetDeadline(time.Time{}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* 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"
|
||||
|
||||
"git.torproject.org/pluggable-transports/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
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* 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"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"hash"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/csrand"
|
||||
"git.torproject.org/pluggable-transports/obfs4.git/common/uniformdh"
|
||||
)
|
||||
|
||||
const (
|
||||
minHandshakeLength = uniformdh.Size + macLength*2
|
||||
maxHandshakeLength = 1532
|
||||
dhMinPadLength = 0
|
||||
dhMaxPadLength = 1308
|
||||
macLength = 128 / 8 // HMAC-SHA256-128()
|
||||
|
||||
kdfSecretLength = keyLength * 2
|
||||
)
|
||||
|
||||
var (
|
||||
errMarkNotFoundYet = errors.New("mark not found yet")
|
||||
|
||||
// ErrInvalidHandshake is the error returned when the handshake fails.
|
||||
ErrInvalidHandshake = errors.New("invalid handshake")
|
||||
)
|
||||
|
||||
type ssDHClientHandshake struct {
|
||||
mac hash.Hash
|
||||
keypair *uniformdh.PrivateKey
|
||||
epochHour []byte
|
||||
padLen int
|
||||
|
||||
serverPublicKey *uniformdh.PublicKey
|
||||
serverMark []byte
|
||||
}
|
||||
|
||||
func (hs *ssDHClientHandshake) generateHandshake() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
hs.mac.Reset()
|
||||
|
||||
// The client handshake is X | P_C | M_C | MAC(X | P_C | M_C | E)
|
||||
x, err := hs.keypair.PublicKey.Bytes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hs.mac.Write(x)
|
||||
mC := hs.mac.Sum(nil)[:macLength]
|
||||
pC, err := makePad(hs.padLen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write X, P_C, M_C.
|
||||
buf.Write(x)
|
||||
buf.Write(pC)
|
||||
buf.Write(mC)
|
||||
|
||||
// Calculate and write the MAC.
|
||||
hs.epochHour = []byte(strconv.FormatInt(getEpochHour(), 10))
|
||||
hs.mac.Write(pC)
|
||||
hs.mac.Write(mC)
|
||||
hs.mac.Write(hs.epochHour)
|
||||
buf.Write(hs.mac.Sum(nil)[:macLength])
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (hs *ssDHClientHandshake) parseServerHandshake(resp []byte) (int, []byte, error) {
|
||||
if len(resp) < minHandshakeLength {
|
||||
return 0, nil, errMarkNotFoundYet
|
||||
}
|
||||
|
||||
// The server response is Y | P_S | M_S | MAC(Y | P_S | M_S | E).
|
||||
if hs.serverPublicKey == nil {
|
||||
y := resp[:uniformdh.Size]
|
||||
|
||||
// Pull out the public key, and derive the server mark.
|
||||
hs.serverPublicKey = &uniformdh.PublicKey{}
|
||||
err := hs.serverPublicKey.SetBytes(y)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
hs.mac.Reset()
|
||||
hs.mac.Write(y)
|
||||
hs.serverMark = hs.mac.Sum(nil)[:macLength]
|
||||
}
|
||||
|
||||
// Find the mark+MAC, if it exits.
|
||||
endPos := len(resp)
|
||||
if endPos > maxHandshakeLength-macLength {
|
||||
endPos = maxHandshakeLength - macLength
|
||||
}
|
||||
pos := bytes.Index(resp[uniformdh.Size:endPos], hs.serverMark)
|
||||
if pos == -1 {
|
||||
if len(resp) >= maxHandshakeLength {
|
||||
// Couldn't find the mark in a maximum length response.
|
||||
return 0, nil, ErrInvalidHandshake
|
||||
}
|
||||
return 0, nil, errMarkNotFoundYet
|
||||
} else if len(resp) < pos+2*macLength {
|
||||
// Didn't receive the full M_S.
|
||||
return 0, nil, errMarkNotFoundYet
|
||||
}
|
||||
pos += uniformdh.Size
|
||||
|
||||
// Validate the MAC.
|
||||
hs.mac.Write(resp[uniformdh.Size : pos+macLength])
|
||||
hs.mac.Write(hs.epochHour)
|
||||
macCmp := hs.mac.Sum(nil)[:macLength]
|
||||
macRx := resp[pos+macLength : pos+2*macLength]
|
||||
if !hmac.Equal(macCmp, macRx) {
|
||||
return 0, nil, ErrInvalidHandshake
|
||||
}
|
||||
|
||||
// Derive the shared secret.
|
||||
ss, err := uniformdh.Handshake(hs.keypair, hs.serverPublicKey)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
seed := sha256.Sum256(ss)
|
||||
return pos + 2*macLength, seed[:], nil
|
||||
}
|
||||
|
||||
func newDHClientHandshake(kB *ssSharedSecret, sessionKey *uniformdh.PrivateKey) *ssDHClientHandshake {
|
||||
hs := &ssDHClientHandshake{keypair: sessionKey}
|
||||
hs.mac = hmac.New(sha256.New, kB[:])
|
||||
hs.padLen = csrand.IntRange(dhMinPadLength, dhMaxPadLength)
|
||||
return hs
|
||||
}
|
||||
|
||||
func getEpochHour() int64 {
|
||||
return time.Now().Unix() / 3600
|
||||
}
|
||||
|
||||
func makePad(padLen int) ([]byte, error) {
|
||||
pad := make([]byte, padLen)
|
||||
if err := csrand.Bytes(pad); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pad, nil
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (c) 2015, Yawning Angel <yawning at torproject dot org>
|
||||
* 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 (
|
||||
"crypto/hmac"
|
||||
"hash"
|
||||
)
|
||||
|
||||
func hkdfExpand(hashFn func() hash.Hash, prk []byte, info []byte, l int) []byte {
|
||||
// Why, yes. golang.org/x/crypto/hkdf exists, and is a fine
|
||||
// implementation of HKDF. However it does both the extract
|
||||
// and expand, while ScrambleSuit only does extract, with no
|
||||
// way to separate the two steps.
|
||||
|
||||
h := hmac.New(hashFn, prk)
|
||||
digestSz := h.Size()
|
||||
if l > 255*digestSz {
|
||||
panic("hkdf: requested OKM length > 255*HashLen")
|
||||
}
|
||||
|
||||
var t []byte
|
||||
okm := make([]byte, 0, l)
|
||||
toAppend := l
|
||||
ctr := byte(1)
|
||||
for toAppend > 0 {
|
||||
h.Reset()
|
||||
h.Write(t)
|
||||
h.Write(info)
|
||||
h.Write([]byte{ctr})
|
||||
t = h.Sum(nil)
|
||||
ctr++
|
||||
|
||||
aLen := digestSz
|
||||
if toAppend < digestSz {
|
||||
aLen = toAppend
|
||||
}
|
||||
okm = append(okm, t[:aLen]...)
|
||||
toAppend -= aLen
|
||||
}
|
||||
return okm
|
||||
}
|
Loading…
Reference in New Issue