Coverage for /pythoncovmergedfiles/medio/medio/src/paramiko/paramiko/packet.py: 41%

353 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:36 +0000

1# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com> 

2# 

3# This file is part of paramiko. 

4# 

5# Paramiko is free software; you can redistribute it and/or modify it under the 

6# terms of the GNU Lesser General Public License as published by the Free 

7# Software Foundation; either version 2.1 of the License, or (at your option) 

8# any later version. 

9# 

10# Paramiko is distributed in the hope that it will be useful, but WITHOUT ANY 

11# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 

12# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 

13# details. 

14# 

15# You should have received a copy of the GNU Lesser General Public License 

16# along with Paramiko; if not, write to the Free Software Foundation, Inc., 

17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 

18 

19""" 

20Packet handling 

21""" 

22 

23import errno 

24import os 

25import socket 

26import struct 

27import threading 

28import time 

29from hmac import HMAC 

30 

31from paramiko import util 

32from paramiko.common import ( 

33 linefeed_byte, 

34 cr_byte_value, 

35 MSG_NAMES, 

36 DEBUG, 

37 xffffffff, 

38 zero_byte, 

39 byte_ord, 

40) 

41from paramiko.util import u 

42from paramiko.ssh_exception import SSHException, ProxyCommandFailure 

43from paramiko.message import Message 

44 

45 

46def compute_hmac(key, message, digest_class): 

47 return HMAC(key, message, digest_class).digest() 

48 

49 

50class NeedRekeyException(Exception): 

51 """ 

52 Exception indicating a rekey is needed. 

53 """ 

54 

55 pass 

56 

57 

58def first_arg(e): 

59 arg = None 

60 if type(e.args) is tuple and len(e.args) > 0: 

61 arg = e.args[0] 

62 return arg 

63 

64 

65class Packetizer: 

66 """ 

67 Implementation of the base SSH packet protocol. 

68 """ 

69 

70 # READ the secsh RFC's before raising these values. if anything, 

71 # they should probably be lower. 

72 REKEY_PACKETS = pow(2, 29) 

73 REKEY_BYTES = pow(2, 29) 

74 

75 # Allow receiving this many packets after a re-key request before 

76 # terminating 

77 REKEY_PACKETS_OVERFLOW_MAX = pow(2, 29) 

78 # Allow receiving this many bytes after a re-key request before terminating 

79 REKEY_BYTES_OVERFLOW_MAX = pow(2, 29) 

80 

81 def __init__(self, socket): 

82 self.__socket = socket 

83 self.__logger = None 

84 self.__closed = False 

85 self.__dump_packets = False 

86 self.__need_rekey = False 

87 self.__init_count = 0 

88 self.__remainder = bytes() 

89 

90 # used for noticing when to re-key: 

91 self.__sent_bytes = 0 

92 self.__sent_packets = 0 

93 self.__received_bytes = 0 

94 self.__received_packets = 0 

95 self.__received_bytes_overflow = 0 

96 self.__received_packets_overflow = 0 

97 

98 # current inbound/outbound ciphering: 

99 self.__block_size_out = 8 

100 self.__block_size_in = 8 

101 self.__mac_size_out = 0 

102 self.__mac_size_in = 0 

103 self.__block_engine_out = None 

104 self.__block_engine_in = None 

105 self.__sdctr_out = False 

106 self.__mac_engine_out = None 

107 self.__mac_engine_in = None 

108 self.__mac_key_out = bytes() 

109 self.__mac_key_in = bytes() 

110 self.__compress_engine_out = None 

111 self.__compress_engine_in = None 

112 self.__sequence_number_out = 0 

113 self.__sequence_number_in = 0 

114 self.__etm_out = False 

115 self.__etm_in = False 

116 

117 # lock around outbound writes (packet computation) 

118 self.__write_lock = threading.RLock() 

119 

120 # keepalives: 

121 self.__keepalive_interval = 0 

122 self.__keepalive_last = time.time() 

123 self.__keepalive_callback = None 

124 

125 self.__timer = None 

126 self.__handshake_complete = False 

127 self.__timer_expired = False 

128 

129 @property 

130 def closed(self): 

131 return self.__closed 

132 

133 def set_log(self, log): 

134 """ 

135 Set the Python log object to use for logging. 

136 """ 

137 self.__logger = log 

138 

139 def set_outbound_cipher( 

140 self, 

141 block_engine, 

142 block_size, 

143 mac_engine, 

144 mac_size, 

145 mac_key, 

146 sdctr=False, 

147 etm=False, 

148 ): 

149 """ 

150 Switch outbound data cipher. 

151 :param etm: Set encrypt-then-mac from OpenSSH 

152 """ 

153 self.__block_engine_out = block_engine 

154 self.__sdctr_out = sdctr 

155 self.__block_size_out = block_size 

156 self.__mac_engine_out = mac_engine 

157 self.__mac_size_out = mac_size 

158 self.__mac_key_out = mac_key 

159 self.__sent_bytes = 0 

160 self.__sent_packets = 0 

161 self.__etm_out = etm 

162 # wait until the reset happens in both directions before clearing 

163 # rekey flag 

164 self.__init_count |= 1 

165 if self.__init_count == 3: 

166 self.__init_count = 0 

167 self.__need_rekey = False 

168 

169 def set_inbound_cipher( 

170 self, 

171 block_engine, 

172 block_size, 

173 mac_engine, 

174 mac_size, 

175 mac_key, 

176 etm=False, 

177 ): 

178 """ 

179 Switch inbound data cipher. 

180 :param etm: Set encrypt-then-mac from OpenSSH 

181 """ 

182 self.__block_engine_in = block_engine 

183 self.__block_size_in = block_size 

184 self.__mac_engine_in = mac_engine 

185 self.__mac_size_in = mac_size 

186 self.__mac_key_in = mac_key 

187 self.__received_bytes = 0 

188 self.__received_packets = 0 

189 self.__received_bytes_overflow = 0 

190 self.__received_packets_overflow = 0 

191 self.__etm_in = etm 

192 # wait until the reset happens in both directions before clearing 

193 # rekey flag 

194 self.__init_count |= 2 

195 if self.__init_count == 3: 

196 self.__init_count = 0 

197 self.__need_rekey = False 

198 

199 def set_outbound_compressor(self, compressor): 

200 self.__compress_engine_out = compressor 

201 

202 def set_inbound_compressor(self, compressor): 

203 self.__compress_engine_in = compressor 

204 

205 def close(self): 

206 self.__closed = True 

207 self.__socket.close() 

208 

209 def set_hexdump(self, hexdump): 

210 self.__dump_packets = hexdump 

211 

212 def get_hexdump(self): 

213 return self.__dump_packets 

214 

215 def get_mac_size_in(self): 

216 return self.__mac_size_in 

217 

218 def get_mac_size_out(self): 

219 return self.__mac_size_out 

220 

221 def need_rekey(self): 

222 """ 

223 Returns ``True`` if a new set of keys needs to be negotiated. This 

224 will be triggered during a packet read or write, so it should be 

225 checked after every read or write, or at least after every few. 

226 """ 

227 return self.__need_rekey 

228 

229 def set_keepalive(self, interval, callback): 

230 """ 

231 Turn on/off the callback keepalive. If ``interval`` seconds pass with 

232 no data read from or written to the socket, the callback will be 

233 executed and the timer will be reset. 

234 """ 

235 self.__keepalive_interval = interval 

236 self.__keepalive_callback = callback 

237 self.__keepalive_last = time.time() 

238 

239 def read_timer(self): 

240 self.__timer_expired = True 

241 

242 def start_handshake(self, timeout): 

243 """ 

244 Tells `Packetizer` that the handshake process started. 

245 Starts a book keeping timer that can signal a timeout in the 

246 handshake process. 

247 

248 :param float timeout: amount of seconds to wait before timing out 

249 """ 

250 if not self.__timer: 

251 self.__timer = threading.Timer(float(timeout), self.read_timer) 

252 self.__timer.start() 

253 

254 def handshake_timed_out(self): 

255 """ 

256 Checks if the handshake has timed out. 

257 

258 If `start_handshake` wasn't called before the call to this function, 

259 the return value will always be `False`. If the handshake completed 

260 before a timeout was reached, the return value will be `False` 

261 

262 :return: handshake time out status, as a `bool` 

263 """ 

264 if not self.__timer: 

265 return False 

266 if self.__handshake_complete: 

267 return False 

268 return self.__timer_expired 

269 

270 def complete_handshake(self): 

271 """ 

272 Tells `Packetizer` that the handshake has completed. 

273 """ 

274 if self.__timer: 

275 self.__timer.cancel() 

276 self.__timer_expired = False 

277 self.__handshake_complete = True 

278 

279 def read_all(self, n, check_rekey=False): 

280 """ 

281 Read as close to N bytes as possible, blocking as long as necessary. 

282 

283 :param int n: number of bytes to read 

284 :return: the data read, as a `str` 

285 

286 :raises: 

287 ``EOFError`` -- if the socket was closed before all the bytes could 

288 be read 

289 """ 

290 out = bytes() 

291 # handle over-reading from reading the banner line 

292 if len(self.__remainder) > 0: 

293 out = self.__remainder[:n] 

294 self.__remainder = self.__remainder[n:] 

295 n -= len(out) 

296 while n > 0: 

297 got_timeout = False 

298 if self.handshake_timed_out(): 

299 raise EOFError() 

300 try: 

301 x = self.__socket.recv(n) 

302 if len(x) == 0: 

303 raise EOFError() 

304 out += x 

305 n -= len(x) 

306 except socket.timeout: 

307 got_timeout = True 

308 except socket.error as e: 

309 # on Linux, sometimes instead of socket.timeout, we get 

310 # EAGAIN. this is a bug in recent (> 2.6.9) kernels but 

311 # we need to work around it. 

312 arg = first_arg(e) 

313 if arg == errno.EAGAIN: 

314 got_timeout = True 

315 elif self.__closed: 

316 raise EOFError() 

317 else: 

318 raise 

319 if got_timeout: 

320 if self.__closed: 

321 raise EOFError() 

322 if check_rekey and (len(out) == 0) and self.__need_rekey: 

323 raise NeedRekeyException() 

324 self._check_keepalive() 

325 return out 

326 

327 def write_all(self, out): 

328 self.__keepalive_last = time.time() 

329 iteration_with_zero_as_return_value = 0 

330 while len(out) > 0: 

331 retry_write = False 

332 try: 

333 n = self.__socket.send(out) 

334 except socket.timeout: 

335 retry_write = True 

336 except socket.error as e: 

337 arg = first_arg(e) 

338 if arg == errno.EAGAIN: 

339 retry_write = True 

340 else: 

341 n = -1 

342 except ProxyCommandFailure: 

343 raise # so it doesn't get swallowed by the below catchall 

344 except Exception: 

345 # could be: (32, 'Broken pipe') 

346 n = -1 

347 if retry_write: 

348 n = 0 

349 if self.__closed: 

350 n = -1 

351 else: 

352 if n == 0 and iteration_with_zero_as_return_value > 10: 

353 # We shouldn't retry the write, but we didn't 

354 # manage to send anything over the socket. This might be an 

355 # indication that we have lost contact with the remote 

356 # side, but are yet to receive an EOFError or other socket 

357 # errors. Let's give it some iteration to try and catch up. 

358 n = -1 

359 iteration_with_zero_as_return_value += 1 

360 if n < 0: 

361 raise EOFError() 

362 if n == len(out): 

363 break 

364 out = out[n:] 

365 return 

366 

367 def readline(self, timeout): 

368 """ 

369 Read a line from the socket. We assume no data is pending after the 

370 line, so it's okay to attempt large reads. 

371 """ 

372 buf = self.__remainder 

373 while linefeed_byte not in buf: 

374 buf += self._read_timeout(timeout) 

375 n = buf.index(linefeed_byte) 

376 self.__remainder = buf[n + 1 :] 

377 buf = buf[:n] 

378 if (len(buf) > 0) and (buf[-1] == cr_byte_value): 

379 buf = buf[:-1] 

380 return u(buf) 

381 

382 def send_message(self, data): 

383 """ 

384 Write a block of data using the current cipher, as an SSH block. 

385 """ 

386 # encrypt this sucka 

387 data = data.asbytes() 

388 cmd = byte_ord(data[0]) 

389 if cmd in MSG_NAMES: 

390 cmd_name = MSG_NAMES[cmd] 

391 else: 

392 cmd_name = "${:x}".format(cmd) 

393 orig_len = len(data) 

394 self.__write_lock.acquire() 

395 try: 

396 if self.__compress_engine_out is not None: 

397 data = self.__compress_engine_out(data) 

398 packet = self._build_packet(data) 

399 if self.__dump_packets: 

400 self._log( 

401 DEBUG, 

402 "Write packet <{}>, length {}".format(cmd_name, orig_len), 

403 ) 

404 self._log(DEBUG, util.format_binary(packet, "OUT: ")) 

405 if self.__block_engine_out is not None: 

406 if self.__etm_out: 

407 # packet length is not encrypted in EtM 

408 out = packet[0:4] + self.__block_engine_out.update( 

409 packet[4:] 

410 ) 

411 else: 

412 out = self.__block_engine_out.update(packet) 

413 else: 

414 out = packet 

415 # + mac 

416 if self.__block_engine_out is not None: 

417 packed = struct.pack(">I", self.__sequence_number_out) 

418 payload = packed + (out if self.__etm_out else packet) 

419 out += compute_hmac( 

420 self.__mac_key_out, payload, self.__mac_engine_out 

421 )[: self.__mac_size_out] 

422 self.__sequence_number_out = ( 

423 self.__sequence_number_out + 1 

424 ) & xffffffff 

425 self.write_all(out) 

426 

427 self.__sent_bytes += len(out) 

428 self.__sent_packets += 1 

429 sent_too_much = ( 

430 self.__sent_packets >= self.REKEY_PACKETS 

431 or self.__sent_bytes >= self.REKEY_BYTES 

432 ) 

433 if sent_too_much and not self.__need_rekey: 

434 # only ask once for rekeying 

435 msg = "Rekeying (hit {} packets, {} bytes sent)" 

436 self._log( 

437 DEBUG, msg.format(self.__sent_packets, self.__sent_bytes) 

438 ) 

439 self.__received_bytes_overflow = 0 

440 self.__received_packets_overflow = 0 

441 self._trigger_rekey() 

442 finally: 

443 self.__write_lock.release() 

444 

445 def read_message(self): 

446 """ 

447 Only one thread should ever be in this function (no other locking is 

448 done). 

449 

450 :raises: `.SSHException` -- if the packet is mangled 

451 :raises: `.NeedRekeyException` -- if the transport should rekey 

452 """ 

453 header = self.read_all(self.__block_size_in, check_rekey=True) 

454 if self.__etm_in: 

455 packet_size = struct.unpack(">I", header[:4])[0] 

456 remaining = packet_size - self.__block_size_in + 4 

457 packet = header[4:] + self.read_all(remaining, check_rekey=False) 

458 mac = self.read_all(self.__mac_size_in, check_rekey=False) 

459 mac_payload = ( 

460 struct.pack(">II", self.__sequence_number_in, packet_size) 

461 + packet 

462 ) 

463 my_mac = compute_hmac( 

464 self.__mac_key_in, mac_payload, self.__mac_engine_in 

465 )[: self.__mac_size_in] 

466 if not util.constant_time_bytes_eq(my_mac, mac): 

467 raise SSHException("Mismatched MAC") 

468 header = packet 

469 

470 if self.__block_engine_in is not None: 

471 header = self.__block_engine_in.update(header) 

472 if self.__dump_packets: 

473 self._log(DEBUG, util.format_binary(header, "IN: ")) 

474 

475 # When ETM is in play, we've already read the packet size & decrypted 

476 # everything, so just set the packet back to the header we obtained. 

477 if self.__etm_in: 

478 packet = header 

479 # Otherwise, use the older non-ETM logic 

480 else: 

481 packet_size = struct.unpack(">I", header[:4])[0] 

482 

483 # leftover contains decrypted bytes from the first block (after the 

484 # length field) 

485 leftover = header[4:] 

486 if (packet_size - len(leftover)) % self.__block_size_in != 0: 

487 raise SSHException("Invalid packet blocking") 

488 buf = self.read_all( 

489 packet_size + self.__mac_size_in - len(leftover) 

490 ) 

491 packet = buf[: packet_size - len(leftover)] 

492 post_packet = buf[packet_size - len(leftover) :] 

493 

494 if self.__block_engine_in is not None: 

495 packet = self.__block_engine_in.update(packet) 

496 packet = leftover + packet 

497 

498 if self.__dump_packets: 

499 self._log(DEBUG, util.format_binary(packet, "IN: ")) 

500 

501 if self.__mac_size_in > 0 and not self.__etm_in: 

502 mac = post_packet[: self.__mac_size_in] 

503 mac_payload = ( 

504 struct.pack(">II", self.__sequence_number_in, packet_size) 

505 + packet 

506 ) 

507 my_mac = compute_hmac( 

508 self.__mac_key_in, mac_payload, self.__mac_engine_in 

509 )[: self.__mac_size_in] 

510 if not util.constant_time_bytes_eq(my_mac, mac): 

511 raise SSHException("Mismatched MAC") 

512 padding = byte_ord(packet[0]) 

513 payload = packet[1 : packet_size - padding] 

514 

515 if self.__dump_packets: 

516 self._log( 

517 DEBUG, 

518 "Got payload ({} bytes, {} padding)".format( 

519 packet_size, padding 

520 ), 

521 ) 

522 

523 if self.__compress_engine_in is not None: 

524 payload = self.__compress_engine_in(payload) 

525 

526 msg = Message(payload[1:]) 

527 msg.seqno = self.__sequence_number_in 

528 self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff 

529 

530 # check for rekey 

531 raw_packet_size = packet_size + self.__mac_size_in + 4 

532 self.__received_bytes += raw_packet_size 

533 self.__received_packets += 1 

534 if self.__need_rekey: 

535 # we've asked to rekey -- give them some packets to comply before 

536 # dropping the connection 

537 self.__received_bytes_overflow += raw_packet_size 

538 self.__received_packets_overflow += 1 

539 if ( 

540 self.__received_packets_overflow 

541 >= self.REKEY_PACKETS_OVERFLOW_MAX 

542 ) or ( 

543 self.__received_bytes_overflow >= self.REKEY_BYTES_OVERFLOW_MAX 

544 ): 

545 raise SSHException( 

546 "Remote transport is ignoring rekey requests" 

547 ) 

548 elif (self.__received_packets >= self.REKEY_PACKETS) or ( 

549 self.__received_bytes >= self.REKEY_BYTES 

550 ): 

551 # only ask once for rekeying 

552 err = "Rekeying (hit {} packets, {} bytes received)" 

553 self._log( 

554 DEBUG, 

555 err.format(self.__received_packets, self.__received_bytes), 

556 ) 

557 self.__received_bytes_overflow = 0 

558 self.__received_packets_overflow = 0 

559 self._trigger_rekey() 

560 

561 cmd = byte_ord(payload[0]) 

562 if cmd in MSG_NAMES: 

563 cmd_name = MSG_NAMES[cmd] 

564 else: 

565 cmd_name = "${:x}".format(cmd) 

566 if self.__dump_packets: 

567 self._log( 

568 DEBUG, 

569 "Read packet <{}>, length {}".format(cmd_name, len(payload)), 

570 ) 

571 return cmd, msg 

572 

573 # ...protected... 

574 

575 def _log(self, level, msg): 

576 if self.__logger is None: 

577 return 

578 if issubclass(type(msg), list): 

579 for m in msg: 

580 self.__logger.log(level, m) 

581 else: 

582 self.__logger.log(level, msg) 

583 

584 def _check_keepalive(self): 

585 if ( 

586 not self.__keepalive_interval 

587 or not self.__block_engine_out 

588 or self.__need_rekey 

589 ): 

590 # wait till we're encrypting, and not in the middle of rekeying 

591 return 

592 now = time.time() 

593 if now > self.__keepalive_last + self.__keepalive_interval: 

594 self.__keepalive_callback() 

595 self.__keepalive_last = now 

596 

597 def _read_timeout(self, timeout): 

598 start = time.time() 

599 while True: 

600 try: 

601 x = self.__socket.recv(128) 

602 if len(x) == 0: 

603 raise EOFError() 

604 break 

605 except socket.timeout: 

606 pass 

607 if self.__closed: 

608 raise EOFError() 

609 now = time.time() 

610 if now - start >= timeout: 

611 raise socket.timeout() 

612 return x 

613 

614 def _build_packet(self, payload): 

615 # pad up at least 4 bytes, to nearest block-size (usually 8) 

616 bsize = self.__block_size_out 

617 # do not include payload length in computations for padding in EtM mode 

618 # (payload length won't be encrypted) 

619 addlen = 4 if self.__etm_out else 8 

620 padding = 3 + bsize - ((len(payload) + addlen) % bsize) 

621 packet = struct.pack(">IB", len(payload) + padding + 1, padding) 

622 packet += payload 

623 if self.__sdctr_out or self.__block_engine_out is None: 

624 # cute trick i caught openssh doing: if we're not encrypting or 

625 # SDCTR mode (RFC4344), 

626 # don't waste random bytes for the padding 

627 packet += zero_byte * padding 

628 else: 

629 packet += os.urandom(padding) 

630 return packet 

631 

632 def _trigger_rekey(self): 

633 # outside code should check for this flag 

634 self.__need_rekey = True