|
| 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 message. |
| 213 | + _, _ = hash.Write(message) |
| 214 | + |
| 215 | + // Include payloads including our own. |
| 216 | + _, _ = hash.Write(payloads[:(NumMaxHops-position)*payloadLen]) |
| 217 | + |
| 218 | + // Include downstream hmacs. |
| 219 | + var hmacsIdx = position + NumMaxHops |
| 220 | + for j := 0; j < NumMaxHops-position-1; j++ { |
| 221 | + _, _ = hash.Write( |
| 222 | + hmacs[hmacsIdx*sha256.Size : (hmacsIdx+1)*sha256.Size], |
| 223 | + ) |
| 224 | + |
| 225 | + hmacsIdx += NumMaxHops - j - 1 |
| 226 | + } |
| 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