| 
 | 1 | +package sphinx  | 
 | 2 | + | 
 | 3 | +import (  | 
 | 4 | +	"bytes"  | 
 | 5 | +	"crypto/hmac"  | 
 | 6 | +	"crypto/sha256"  | 
 | 7 | +	"encoding/binary"  | 
 | 8 | +	"errors"  | 
 | 9 | +	"fmt"  | 
 | 10 | +)  | 
 | 11 | + | 
 | 12 | +var byteOrder = binary.BigEndian  | 
 | 13 | + | 
 | 14 | +// DecryptError attempts to decrypt the passed encrypted error response. The  | 
 | 15 | +// onion failure is encrypted in backward manner, starting from the node where  | 
 | 16 | +// error have occurred. As a result, in order to decrypt the error we need get  | 
 | 17 | +// all shared secret and apply decryption in the reverse order. A structure is  | 
 | 18 | +// returned that contains the decrypted error message and information on the  | 
 | 19 | +// sender.  | 
 | 20 | +func (o *OnionErrorDecrypter) DecryptFatError(encryptedData []byte) (  | 
 | 21 | +	*DecryptedError, error) {  | 
 | 22 | + | 
 | 23 | +	// Ensure the error message length is enough to contain the payloads and  | 
 | 24 | +	// hmacs blocks. Otherwise blame the first hop.  | 
 | 25 | +	if len(encryptedData) < 256+hmacsAndPayloadsLen {  | 
 | 26 | +		return &DecryptedError{  | 
 | 27 | +			SenderIdx: 1,  | 
 | 28 | +			Sender:    o.circuit.PaymentPath[0],  | 
 | 29 | +		}, nil  | 
 | 30 | +	}  | 
 | 31 | + | 
 | 32 | +	sharedSecrets, err := generateSharedSecrets(  | 
 | 33 | +		o.circuit.PaymentPath,  | 
 | 34 | +		o.circuit.SessionKey,  | 
 | 35 | +	)  | 
 | 36 | +	if err != nil {  | 
 | 37 | +		return nil, fmt.Errorf("error generating shared secret: %w", err)  | 
 | 38 | +	}  | 
 | 39 | + | 
 | 40 | +	var (  | 
 | 41 | +		sender      int  | 
 | 42 | +		msg         []byte  | 
 | 43 | +		dummySecret Hash256  | 
 | 44 | +	)  | 
 | 45 | +	copy(dummySecret[:], bytes.Repeat([]byte{1}, 32))  | 
 | 46 | + | 
 | 47 | +	// We'll iterate a constant amount of hops to ensure that we don't give  | 
 | 48 | +	// away an timing information pertaining to the position in the route  | 
 | 49 | +	// that the error emanated from.  | 
 | 50 | +	holdTimesMs := make([]uint64, 0)  | 
 | 51 | +	for i := 0; i < NumMaxHops; i++ {  | 
 | 52 | +		var sharedSecret Hash256  | 
 | 53 | + | 
 | 54 | +		// If we've already found the sender, then we'll use our dummy  | 
 | 55 | +		// secret to continue decryption attempts to fill out the rest  | 
 | 56 | +		// of the loop. Otherwise, we'll use the next shared secret in  | 
 | 57 | +		// line.  | 
 | 58 | +		if sender != 0 || i > len(sharedSecrets)-1 {  | 
 | 59 | +			sharedSecret = dummySecret  | 
 | 60 | +		} else {  | 
 | 61 | +			sharedSecret = sharedSecrets[i]  | 
 | 62 | +		}  | 
 | 63 | + | 
 | 64 | +		// With the shared secret, we'll now strip off a layer of  | 
 | 65 | +		// encryption from the encrypted error payload.  | 
 | 66 | +		encryptedData = onionEncrypt(&sharedSecret, encryptedData)  | 
 | 67 | + | 
 | 68 | +		message, payloads, hmacs := getMsgComponents(encryptedData)  | 
 | 69 | + | 
 | 70 | +		expectedHmac := calculateHmac(sharedSecret, i, message, payloads, hmacs)  | 
 | 71 | +		actualHmac := hmacs[i*sha256.Size : (i+1)*sha256.Size]  | 
 | 72 | + | 
 | 73 | +		// If the hmac does not match up, exit with a nil message.  | 
 | 74 | +		if !bytes.Equal(actualHmac, expectedHmac) && sender == 0 {  | 
 | 75 | +			sender = i + 1  | 
 | 76 | +			msg = nil  | 
 | 77 | +		}  | 
 | 78 | + | 
 | 79 | +		// Extract the payload and exit with a nil message if it is invalid.  | 
 | 80 | +		payloadType, holdTimeMs, err := extractPayload(payloads)  | 
 | 81 | +		if sender == 0 {  | 
 | 82 | +			if err != nil {  | 
 | 83 | +				sender = i + 1  | 
 | 84 | +				msg = nil  | 
 | 85 | +			}  | 
 | 86 | + | 
 | 87 | +			// Store hold time reported by this node.  | 
 | 88 | +			holdTimesMs = append(holdTimesMs, holdTimeMs)  | 
 | 89 | + | 
 | 90 | +			// If we are at the node that is the source of the error, we can now  | 
 | 91 | +			// save the message in our return variable.  | 
 | 92 | +			if payloadType == payloadFinal {  | 
 | 93 | +				sender = i + 1  | 
 | 94 | +				msg = message  | 
 | 95 | +			}  | 
 | 96 | +		}  | 
 | 97 | + | 
 | 98 | +		// Shift payloads and hmacs to the left to prepare for the next  | 
 | 99 | +		// iteration.  | 
 | 100 | +		shiftPayloadsLeft(payloads)  | 
 | 101 | +		shiftHmacsLeft(hmacs)  | 
 | 102 | +	}  | 
 | 103 | + | 
 | 104 | +	// If the sender index is still zero, all hmacs checked out but none of the  | 
 | 105 | +	// payloads was a final payload. In this case we must be dealing with a max  | 
 | 106 | +	// length route and a final hop that returned an intermediate payload. Blame  | 
 | 107 | +	// the final hop.  | 
 | 108 | +	if sender == 0 {  | 
 | 109 | +		sender = NumMaxHops  | 
 | 110 | +		msg = nil  | 
 | 111 | +	}  | 
 | 112 | + | 
 | 113 | +	return &DecryptedError{  | 
 | 114 | +		SenderIdx:   sender,  | 
 | 115 | +		Sender:      o.circuit.PaymentPath[sender-1],  | 
 | 116 | +		Message:     msg,  | 
 | 117 | +		HoldTimesMs: holdTimesMs,  | 
 | 118 | +	}, nil  | 
 | 119 | +}  | 
 | 120 | + | 
 | 121 | +const (  | 
 | 122 | +	totalHmacs          = (NumMaxHops * (NumMaxHops + 1)) / 2  | 
 | 123 | +	allHmacsLen         = totalHmacs * sha256.Size  | 
 | 124 | +	hmacsAndPayloadsLen = allHmacsLen + allPayloadsLen  | 
 | 125 | + | 
 | 126 | +	// payloadLen is the size of the per-node payload. It consists of a 1-byte  | 
 | 127 | +	// payload type and an 8-byte hold time.  | 
 | 128 | +	payloadLen = 1 + 8  | 
 | 129 | + | 
 | 130 | +	allPayloadsLen = payloadLen * NumMaxHops  | 
 | 131 | + | 
 | 132 | +	payloadFinal        = 1  | 
 | 133 | +	payloadIntermediate = 0  | 
 | 134 | +)  | 
 | 135 | + | 
 | 136 | +func shiftHmacsRight(hmacs []byte) {  | 
 | 137 | +	if len(hmacs) != allHmacsLen {  | 
 | 138 | +		panic("invalid hmac block length")  | 
 | 139 | +	}  | 
 | 140 | + | 
 | 141 | +	srcIdx := totalHmacs - 2  | 
 | 142 | +	destIdx := totalHmacs - 1  | 
 | 143 | +	copyLen := 1  | 
 | 144 | +	for i := 0; i < NumMaxHops-1; i++ {  | 
 | 145 | +		copy(  | 
 | 146 | +			hmacs[destIdx*sha256.Size:],  | 
 | 147 | +			hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size],  | 
 | 148 | +		)  | 
 | 149 | + | 
 | 150 | +		copyLen++  | 
 | 151 | + | 
 | 152 | +		srcIdx -= copyLen + 1  | 
 | 153 | +		destIdx -= copyLen  | 
 | 154 | +	}  | 
 | 155 | +}  | 
 | 156 | + | 
 | 157 | +func shiftHmacsLeft(hmacs []byte) {  | 
 | 158 | +	if len(hmacs) != allHmacsLen {  | 
 | 159 | +		panic("invalid hmac block length")  | 
 | 160 | +	}  | 
 | 161 | + | 
 | 162 | +	srcIdx := NumMaxHops  | 
 | 163 | +	destIdx := 1  | 
 | 164 | +	copyLen := NumMaxHops - 1  | 
 | 165 | +	for i := 0; i < NumMaxHops-1; i++ {  | 
 | 166 | +		copy(  | 
 | 167 | +			hmacs[destIdx*sha256.Size:],  | 
 | 168 | +			hmacs[srcIdx*sha256.Size:(srcIdx+copyLen)*sha256.Size],  | 
 | 169 | +		)  | 
 | 170 | + | 
 | 171 | +		srcIdx += copyLen  | 
 | 172 | +		destIdx += copyLen + 1  | 
 | 173 | +		copyLen--  | 
 | 174 | +	}  | 
 | 175 | +}  | 
 | 176 | + | 
 | 177 | +func shiftPayloadsRight(payloads []byte) {  | 
 | 178 | +	if len(payloads) != allPayloadsLen {  | 
 | 179 | +		panic("invalid payload block length")  | 
 | 180 | +	}  | 
 | 181 | + | 
 | 182 | +	copy(payloads[payloadLen:], payloads)  | 
 | 183 | +}  | 
 | 184 | + | 
 | 185 | +func shiftPayloadsLeft(payloads []byte) {  | 
 | 186 | +	if len(payloads) != allPayloadsLen {  | 
 | 187 | +		panic("invalid payload block length")  | 
 | 188 | +	}  | 
 | 189 | + | 
 | 190 | +	copy(payloads, payloads[payloadLen:NumMaxHops*payloadLen])  | 
 | 191 | +}  | 
 | 192 | + | 
 | 193 | +// getMsgComponents splits a complete failure message into its components  | 
 | 194 | +// without re-allocating memory.  | 
 | 195 | +func getMsgComponents(data []byte) ([]byte, []byte, []byte) {  | 
 | 196 | +	payloads := data[len(data)-hmacsAndPayloadsLen : len(data)-allHmacsLen]  | 
 | 197 | +	hmacs := data[len(data)-allHmacsLen:]  | 
 | 198 | +	message := data[:len(data)-hmacsAndPayloadsLen]  | 
 | 199 | + | 
 | 200 | +	return message, payloads, hmacs  | 
 | 201 | +}  | 
 | 202 | + | 
 | 203 | +// calculateHmac calculates an hmac given a shared secret and a presumed  | 
 | 204 | +// position in the path. Position is expressed as the distance to the error  | 
 | 205 | +// source. The error source itself is at position 0.  | 
 | 206 | +func calculateHmac(sharedSecret Hash256, position int,  | 
 | 207 | +	message, payloads, hmacs []byte) []byte {  | 
 | 208 | + | 
 | 209 | +	umKey := generateKey("um", &sharedSecret)  | 
 | 210 | +	hash := hmac.New(sha256.New, umKey[:])  | 
 | 211 | + | 
 | 212 | +	// Include payloads including our own.  | 
 | 213 | +	_, _ = hash.Write(payloads[:(NumMaxHops-position)*payloadLen])  | 
 | 214 | + | 
 | 215 | +	// Include downstream hmacs.  | 
 | 216 | +	var hmacsIdx = position + NumMaxHops  | 
 | 217 | +	for j := 0; j < NumMaxHops-position-1; j++ {  | 
 | 218 | +		_, _ = hash.Write(  | 
 | 219 | +			hmacs[hmacsIdx*sha256.Size : (hmacsIdx+1)*sha256.Size],  | 
 | 220 | +		)  | 
 | 221 | + | 
 | 222 | +		hmacsIdx += NumMaxHops - j - 1  | 
 | 223 | +	}  | 
 | 224 | + | 
 | 225 | +	// Include message.  | 
 | 226 | +	_, _ = hash.Write(message)  | 
 | 227 | + | 
 | 228 | +	return hash.Sum(nil)  | 
 | 229 | +}  | 
 | 230 | + | 
 | 231 | +// calculateHmac calculates an hmac using the shared secret for this  | 
 | 232 | +// OnionErrorEncryptor instance.  | 
 | 233 | +func (o *OnionErrorEncrypter) calculateHmac(position int,  | 
 | 234 | +	message, payloads, hmacs []byte) []byte {  | 
 | 235 | + | 
 | 236 | +	return calculateHmac(o.sharedSecret, position, message, payloads, hmacs)  | 
 | 237 | +}  | 
 | 238 | + | 
 | 239 | +// addHmacs updates the failure data with a series of hmacs corresponding to all  | 
 | 240 | +// possible positions in the path for the current node.  | 
 | 241 | +func (o *OnionErrorEncrypter) addHmacs(data []byte) {  | 
 | 242 | +	message, payloads, hmacs := getMsgComponents(data)  | 
 | 243 | + | 
 | 244 | +	for i := 0; i < NumMaxHops; i++ {  | 
 | 245 | +		hmac := o.calculateHmac(i, message, payloads, hmacs)  | 
 | 246 | + | 
 | 247 | +		copy(hmacs[i*sha256.Size:], hmac)  | 
 | 248 | +	}  | 
 | 249 | +}  | 
 | 250 | + | 
 | 251 | +// EncryptError is used to make data obfuscation using the generated shared  | 
 | 252 | +// secret.  | 
 | 253 | +//  | 
 | 254 | +// In context of Lightning Network is either used by the nodes in order to make  | 
 | 255 | +// initial obfuscation with the creation of the hmac or by the forwarding nodes  | 
 | 256 | +// for backward failure obfuscation of the onion failure blob. By obfuscating  | 
 | 257 | +// the onion failure on every node in the path we are adding additional step of  | 
 | 258 | +// the security and barrier for malware nodes to retrieve valuable information.  | 
 | 259 | +// The reason for using onion obfuscation is to not give  | 
 | 260 | +// away to the nodes in the payment path the information about the exact  | 
 | 261 | +// failure and its origin.  | 
 | 262 | +func (o *OnionErrorEncrypter) EncryptFatError(initial bool, data []byte,  | 
 | 263 | +	holdTimeMs uint64) []byte {  | 
 | 264 | + | 
 | 265 | +	if initial {  | 
 | 266 | +		data = o.initializePayload(data, holdTimeMs)  | 
 | 267 | +	} else {  | 
 | 268 | +		o.addIntermediatePayload(data, holdTimeMs)  | 
 | 269 | +	}  | 
 | 270 | + | 
 | 271 | +	// Update hmac block.  | 
 | 272 | +	o.addHmacs(data)  | 
 | 273 | + | 
 | 274 | +	// Obfuscate.  | 
 | 275 | +	return onionEncrypt(&o.sharedSecret, data)  | 
 | 276 | +}  | 
 | 277 | + | 
 | 278 | +func (o *OnionErrorEncrypter) initializePayload(message []byte,  | 
 | 279 | +	holdTimeMs uint64) []byte {  | 
 | 280 | + | 
 | 281 | +	// Add space for payloads and hmacs.  | 
 | 282 | +	data := make([]byte, len(message)+hmacsAndPayloadsLen)  | 
 | 283 | +	copy(data, message)  | 
 | 284 | + | 
 | 285 | +	_, payloads, _ := getMsgComponents(data)  | 
 | 286 | + | 
 | 287 | +	// Signal final hops in the payload.  | 
 | 288 | +	addPayload(payloads, payloadFinal, holdTimeMs)  | 
 | 289 | + | 
 | 290 | +	return data  | 
 | 291 | +}  | 
 | 292 | + | 
 | 293 | +func addPayload(payloads []byte, payloadType PayloadType, holdTimeMs uint64) {  | 
 | 294 | +	byteOrder.PutUint64(payloads[1:], holdTimeMs)  | 
 | 295 | + | 
 | 296 | +	payloads[0] = byte(payloadType)  | 
 | 297 | +}  | 
 | 298 | + | 
 | 299 | +func extractPayload(payloads []byte) (PayloadType, uint64, error) {  | 
 | 300 | +	var payloadType PayloadType  | 
 | 301 | + | 
 | 302 | +	switch payloads[0] {  | 
 | 303 | +	case payloadFinal, payloadIntermediate:  | 
 | 304 | +		payloadType = PayloadType(payloads[0])  | 
 | 305 | + | 
 | 306 | +	default:  | 
 | 307 | +		return 0, 0, errors.New("invalid payload type")  | 
 | 308 | +	}  | 
 | 309 | + | 
 | 310 | +	holdTimeMs := byteOrder.Uint64(payloads[1:])  | 
 | 311 | + | 
 | 312 | +	return payloadType, holdTimeMs, nil  | 
 | 313 | +}  | 
 | 314 | + | 
 | 315 | +func (o *OnionErrorEncrypter) addIntermediatePayload(data []byte,  | 
 | 316 | +	holdTimeMs uint64) {  | 
 | 317 | + | 
 | 318 | +	_, payloads, hmacs := getMsgComponents(data)  | 
 | 319 | + | 
 | 320 | +	// Shift hmacs and payloads to create space for the payload.  | 
 | 321 | +	shiftPayloadsRight(payloads)  | 
 | 322 | +	shiftHmacsRight(hmacs)  | 
 | 323 | + | 
 | 324 | +	// Signal intermediate hop in the payload.  | 
 | 325 | +	addPayload(payloads, payloadIntermediate, holdTimeMs)  | 
 | 326 | +}  | 
0 commit comments