From e49f7fe4ef391925e8477e3294bd02b96b2f956e Mon Sep 17 00:00:00 2001 From: IT Date: Fri, 12 Jun 2026 18:22:02 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20SSH=20=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=AE=9E=E7=8E=B0=E7=9A=84=E5=A4=9A=E4=B8=AA=E5=85=B3?= =?UTF-8?q?=E9=94=AE=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ECDH_INIT 发送原始 EC 点而非 SSH 公钥 blob - Exchange hash 计算使用 encodeString 包装所有字段 - NEWKEYS 以明文发送后再启用加密 - AES-GCM 模式下 MAC 算法改为 none - 修复 WebSocket 发送 Uint8Array 时 buffer 偏移问题 --- src/ssh/kex-ecdh.ts | 60 +++++++++++---------------------------- src/ssh/kex.ts | 4 +-- src/worker/ssh-session.ts | 48 +++++++++++++++---------------- 3 files changed, 43 insertions(+), 69 deletions(-) diff --git a/src/ssh/kex-ecdh.ts b/src/ssh/kex-ecdh.ts index 0540496..4c50948 100644 --- a/src/ssh/kex-ecdh.ts +++ b/src/ssh/kex-ecdh.ts @@ -1,5 +1,5 @@ import { SSH_MSG_KEX_ECDH_INIT } from '../types'; -import { concat, readUint32, encodeUint32, encodeString, toSSHMPInt, extractRawECDHPoint } from './utils'; +import { concat, readUint32, encodeString, toSSHMPInt } from './utils'; export class ECDHKeyExchange { static async generateKeyPair(): Promise { @@ -10,44 +10,22 @@ export class ECDHKeyExchange { ); } - static async exportPublicKeyForSSH(keyPair: CryptoKeyPair): Promise { - const rawKey = new Uint8Array( + static async exportRawPublicKey(keyPair: CryptoKeyPair): Promise { + return new Uint8Array( await crypto.subtle.exportKey('raw', keyPair.publicKey) ); - - const parts: Uint8Array[] = []; - - const keyType = new TextEncoder().encode('ecdsa-sha2-nistp256'); - const keyTypeLen = new Uint8Array(4); - new DataView(keyTypeLen.buffer).setUint32(0, keyType.length, false); - - const curveName = new TextEncoder().encode('nistp256'); - const curveNameLen = new Uint8Array(4); - new DataView(curveNameLen.buffer).setUint32(0, curveName.length, false); - - const pointLen = new Uint8Array(4); - new DataView(pointLen.buffer).setUint32(0, rawKey.length, false); - - const totalLen = keyTypeLen.length + keyType.length + - curveNameLen.length + curveName.length + - pointLen.length + rawKey.length; - const totalLenBytes = new Uint8Array(4); - new DataView(totalLenBytes.buffer).setUint32(0, totalLen, false); - - return concat(totalLenBytes, keyTypeLen, keyType, - curveNameLen, curveName, pointLen, rawKey); } - static buildInit(clientPublicKeySSH: Uint8Array): Uint8Array { + static buildInit(clientRawPublicKey: Uint8Array): Uint8Array { return concat( new Uint8Array([SSH_MSG_KEX_ECDH_INIT]), - clientPublicKeySSH + encodeString(clientRawPublicKey) ); } static parseReply(data: Uint8Array): { hostKey: Uint8Array; - serverPublicKey: Uint8Array; + serverRawPublicKey: Uint8Array; signature: Uint8Array; } { let offset = 1; @@ -59,25 +37,23 @@ export class ECDHKeyExchange { const qSLen = readUint32(data, offset); offset += 4; - const serverPublicKey = data.slice(offset, offset + qSLen); + const serverRawPublicKey = data.slice(offset, offset + qSLen); offset += qSLen; const sigLen = readUint32(data, offset); offset += 4; const signature = data.slice(offset, offset + sigLen); - return { hostKey, serverPublicKey, signature }; + return { hostKey, serverRawPublicKey, signature }; } static async computeSharedSecret( privateKey: CryptoKey, - serverPublicKeyRaw: Uint8Array + serverRawPublicKey: Uint8Array ): Promise { - const rawPoint = extractRawECDHPoint(serverPublicKeyRaw); - const serverKey = await crypto.subtle.importKey( 'raw', - rawPoint, + serverRawPublicKey, { name: 'ECDH', namedCurve: 'P-256' }, false, [] @@ -98,20 +74,18 @@ export class ECDHKeyExchange { clientKEXInit: Uint8Array, serverKEXInit: Uint8Array, hostKey: Uint8Array, - clientPublicKeySSH: Uint8Array, - serverPublicKeySSH: Uint8Array, + clientRawPublicKey: Uint8Array, + serverRawPublicKey: Uint8Array, sharedSecret: Uint8Array ): Promise { const data = concat( encodeString(clientVersion), encodeString(serverVersion), - encodeUint32(clientKEXInit.length), - clientKEXInit, - encodeUint32(serverKEXInit.length), - serverKEXInit, - hostKey, - clientPublicKeySSH, - serverPublicKeySSH, + encodeString(clientKEXInit), + encodeString(serverKEXInit), + encodeString(hostKey), + encodeString(clientRawPublicKey), + encodeString(serverRawPublicKey), sharedSecret ); diff --git a/src/ssh/kex.ts b/src/ssh/kex.ts index 2123ee8..2231cfd 100644 --- a/src/ssh/kex.ts +++ b/src/ssh/kex.ts @@ -16,8 +16,8 @@ export class KEXInitBuilder { 'rsa-sha2-256,ssh-rsa', 'aes256-gcm@openssh.com', 'aes256-gcm@openssh.com', - 'hmac-sha2-256-etm@openssh.com,hmac-sha2-256', - 'hmac-sha2-256-etm@openssh.com,hmac-sha2-256', + 'none', + 'none', 'none', 'none', '', diff --git a/src/worker/ssh-session.ts b/src/worker/ssh-session.ts index 3aafd4c..d398ac9 100644 --- a/src/worker/ssh-session.ts +++ b/src/worker/ssh-session.ts @@ -34,6 +34,7 @@ export class SSHSession { private channel: SSHChannel; private encryptCipher: SSHAESGCMCipher | null = null; private decryptCipher: SSHAESGCMCipher | null = null; + private derivedKeys: any = null; private seqNumSend: number = 0; private seqNumRecv: number = 0; @@ -43,7 +44,7 @@ export class SSHSession { private kexInitRemote: Uint8Array | null = null; private ecdhKeyPair!: CryptoKeyPair; - private ecdhPublicKeySSH!: Uint8Array; + private ecdhRawPublicKey!: Uint8Array; private state: 'connecting' | 'version' | 'kex' | 'auth' | 'shell' | 'ready' = 'connecting'; @@ -127,12 +128,12 @@ export class SSHSession { this.ecdhKeyPair = await ECDHKeyExchange.generateKeyPair(); console.log('[KEX] ECDH key pair generated'); - this.ecdhPublicKeySSH = await ECDHKeyExchange.exportPublicKeyForSSH( + this.ecdhRawPublicKey = await ECDHKeyExchange.exportRawPublicKey( this.ecdhKeyPair ); - console.log('[KEX] Public key exported, length:', this.ecdhPublicKeySSH.length); + console.log('[KEX] Raw public key exported, length:', this.ecdhRawPublicKey.length); - const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhPublicKeySSH); + const ecdhInit = ECDHKeyExchange.buildInit(this.ecdhRawPublicKey); const ecdhPacket = SSHPacketBuilder.build( ecdhInit, 8, null, this.seqNumSend++ ); @@ -200,19 +201,18 @@ export class SSHSession { break; case SSH_MSG_NEWKEYS: - console.log('[KEX] Received NEWKEYS'); - await this.enableEncryption(); - console.log('[KEX] Encryption enabled'); + console.log('[KEX] Received NEWKEYS from server'); const newKeys = new Uint8Array([SSH_MSG_NEWKEYS]); const packet = SSHPacketBuilder.build( - newKeys, 16, - (data, seq) => this.encryptCipher!.encrypt(data, seq) as any, - this.seqNumSend++ + newKeys, 8, null, this.seqNumSend++ ); await this.writeSocket(packet); console.log('[KEX] NEWKEYS sent'); + await this.enableEncryption(); + console.log('[KEX] Encryption enabled'); + this.state = 'auth'; console.log('[SSH] State changed to: auth'); await this.authenticate(); @@ -222,16 +222,16 @@ export class SSHSession { private async handleECDHReply(payload: Uint8Array): Promise { console.log('[KEX] Parsing ECDH_REPLY...'); - const { hostKey, serverPublicKey, signature } = + const { hostKey, serverRawPublicKey, signature } = ECDHKeyExchange.parseReply(payload); - console.log('[KEX] ECDH_REPLY parsed'); + console.log('[KEX] ECDH_REPLY parsed, hostKey:', hostKey.length, 'serverPubKey:', serverRawPublicKey.length, 'sig:', signature.length); console.log('[KEX] Computing shared secret...'); const sharedSecret = await ECDHKeyExchange.computeSharedSecret( this.ecdhKeyPair.privateKey, - serverPublicKey + serverRawPublicKey ); - console.log('[KEX] Shared secret computed'); + console.log('[KEX] Shared secret computed, length:', sharedSecret.length); console.log('[KEX] Computing exchange hash...'); const H = await ECDHKeyExchange.computeExchangeHash( @@ -240,8 +240,8 @@ export class SSHSession { this.kexInitLocal!, this.kexInitRemote!, hostKey, - this.ecdhPublicKeySSH, - serverPublicKey, + this.ecdhRawPublicKey, + serverRawPublicKey, sharedSecret ); console.log('[KEX] Exchange hash computed'); @@ -252,10 +252,14 @@ export class SSHSession { } console.log('[KEX] Deriving keys...'); - const keys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!); - console.log('[KEX] Keys derived'); + this.derivedKeys = await KeyDerivation.deriveKeys(sharedSecret, H, this.sessionID!); + console.log('[KEX] Keys derived, waiting for NEWKEYS...'); + } + + private async enableEncryption(): Promise { + console.log('[KEX] Enabling encryption...'); + const keys = this.derivedKeys; - console.log('[KEX] Initializing encryption ciphers...'); this.encryptCipher = new SSHAESGCMCipher( keys.encKeyClientToServer, keys.ivClientToServer @@ -270,10 +274,6 @@ export class SSHSession { console.log('[KEX] Encryption ciphers ready'); } - private async enableEncryption(): Promise { - console.log('[KEX] Enabling encryption...'); - } - private async authenticate(): Promise { console.log('[AUTH] Sending password authentication...'); const authRequest = SSHAuth.buildPasswordAuthRequest( @@ -347,7 +347,7 @@ export class SSHSession { case SSH_MSG_CHANNEL_DATA: const outputData = this.channel.handleChannelData(payload); - this.ws.send(outputData.buffer); + this.ws.send(outputData.slice().buffer); break; case SSH_MSG_CHANNEL_WINDOW_ADJUST: