@electerm/ssh2 1.17.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -244,6 +244,8 @@ class Client extends EventEmitter {
244
244
  ? cfg.debug
245
245
  : undefined);
246
246
 
247
+ this.config.sftpEncoding = cfg.sftpEncoding || 'utf8';
248
+
247
249
  if (cfg.agentForward === true && !this.config.allowAgentFwd) {
248
250
  throw new Error(
249
251
  'You must set a valid agent path to allow agent forwarding'
@@ -604,7 +606,7 @@ class Client extends EventEmitter {
604
606
  };
605
607
  const instance = (
606
608
  isSFTP
607
- ? new SFTP(this, chanInfo, { debug })
609
+ ? new SFTP(this, chanInfo, { debug, encoding: this.config.sftpEncoding })
608
610
  : new Channel(this, chanInfo)
609
611
  );
610
612
  this._chanMgr.update(info.recipient, instance);
@@ -629,35 +629,65 @@ class Protocol {
629
629
  sendPacket(this, this._packetRW.write.finalize(packet));
630
630
  }
631
631
  authPK(username, pubKey, keyAlgo, cbSign) {
632
+ if (pubKey && pubKey.certificate) {
633
+ return this.authPKCert(username, pubKey, keyAlgo, cbSign);
634
+ }
635
+ if (this._server)
636
+ throw new Error('Client-only method called in server mode');
637
+
638
+ pubKey = parseKey(pubKey);
639
+ if (pubKey instanceof Error)
640
+ throw new Error('Invalid key');
641
+
642
+ let keyType = pubKey.type;
643
+ if (keyType === 'ssh-rsa') {
644
+ for (const algo of ['rsa-sha2-512', 'rsa-sha2-256']) {
645
+ if (this._remoteHostKeyAlgorithms.includes(algo)) {
646
+ keyType = algo;
647
+ break;
648
+ }
649
+ }
650
+ }
651
+ pubKey = pubKey.getPublicSSH();
652
+
653
+ if (typeof keyAlgo === 'function') {
654
+ cbSign = keyAlgo;
655
+ keyAlgo = undefined;
656
+ }
657
+ if (!keyAlgo)
658
+ keyAlgo = keyType;
659
+
660
+ // For standard public keys, the algorithm name on the wire (keyAlgo)
661
+ // is the same as the algorithm used for signing (signAlgo).
662
+ this._authPKCommon(username, keyAlgo, pubKey, keyAlgo, cbSign);
663
+ }
664
+
665
+ authPKCert(username, pubKey, keyAlgo, cbSign) {
632
666
  if (this._server)
633
667
  throw new Error('Client-only method called in server mode');
634
668
 
635
- const origPubKey = pubKey;
636
-
637
669
  // Only parse if it's not already a parsed key object
638
- // Check for common key object properties
639
- if (typeof pubKey !== 'object' || pubKey === null ||
670
+ if (typeof pubKey !== 'object' || pubKey === null ||
640
671
  typeof pubKey.type !== 'string' || typeof pubKey.getPublicSSH !== 'function') {
641
672
  pubKey = parseKey(pubKey);
642
673
  if (pubKey instanceof Error)
643
674
  throw new Error('Invalid key');
644
675
  }
645
676
 
646
- let keyType = pubKey.type;
647
-
648
677
  // Check if this is a certificate-wrapped key
649
- let pubKeyData = pubKey.getPublicSSH();
678
+ let pubKeyData;
650
679
  let isCertificate = false;
651
680
  if (pubKey.getCertificateBuffer) {
652
- // This is a CertificateKey, use the certificate data instead
653
681
  pubKeyData = pubKey.getCertificateBuffer();
654
682
  isCertificate = true;
655
683
  this._debug && this._debug('Using SSH certificate for authentication');
684
+ } else {
685
+ pubKeyData = pubKey.getPublicSSH();
656
686
  }
657
687
 
658
- // Upgrade RSA to SHA2 variants if server supports them
659
- // signAlgo is the actual algorithm used for signing
660
- // NOTE: Order must match getKeyAlgos() in client.js - rsa-sha2-256 first
688
+ let keyType = pubKey.type;
689
+
690
+ // Determine the base signing algorithm (upgrading RSA if needed)
661
691
  let signAlgo = keyType;
662
692
  if (keyType === 'ssh-rsa') {
663
693
  for (const algo of ['rsa-sha2-256', 'rsa-sha2-512']) {
@@ -667,12 +697,10 @@ class Protocol {
667
697
  }
668
698
  }
669
699
  }
670
-
671
- // For certificates, append the cert suffix to the algorithm for the packet
672
- // but keep signAlgo as the base signing algorithm
700
+
701
+ // Determine the algorithm name to use in the packet (keyAlgo)
673
702
  if (isCertificate) {
674
703
  // Map the signing algorithm to certificate algorithm
675
- // e.g., rsa-sha2-512 -> rsa-sha2-512-cert-v01@openssh.com
676
704
  if (signAlgo === 'rsa-sha2-512') {
677
705
  keyType = 'rsa-sha2-512-cert-v01@openssh.com';
678
706
  } else if (signAlgo === 'rsa-sha2-256') {
@@ -692,7 +720,6 @@ class Protocol {
692
720
  }
693
721
  this._debug && this._debug(`Certificate key algorithm: ${keyType}`);
694
722
  } else {
695
- // For non-certificates, keyType should be signAlgo
696
723
  keyType = signAlgo;
697
724
  }
698
725
 
@@ -700,118 +727,119 @@ class Protocol {
700
727
  cbSign = keyAlgo;
701
728
  keyAlgo = undefined;
702
729
  }
703
-
704
- // For certificates, we must use the certificate algorithm regardless of what was passed
730
+
731
+ // For certificates, default to the determined cert type if not provided
705
732
  if (isCertificate) {
706
733
  keyAlgo = keyType;
707
734
  } else if (!keyAlgo) {
708
735
  keyAlgo = keyType;
709
736
  }
710
737
 
711
- const userLen = Buffer.byteLength(username);
712
- const algoLen = Buffer.byteLength(keyAlgo);
713
- // For certificates, signAlgo is the base signing algorithm (e.g., rsa-sha2-512)
714
- // For non-certificates, signAlgo equals keyAlgo
715
- const signAlgoLen = Buffer.byteLength(signAlgo);
716
- const pubKeyLen = pubKeyData.length;
717
- const sessionID = this._kex.sessionID;
718
- const sesLen = sessionID.length;
719
- const payloadLen =
720
- (cbSign ? 4 + sesLen : 0)
721
- + 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen;
722
- let packet;
723
- let p;
724
- if (cbSign) {
725
- packet = Buffer.allocUnsafe(payloadLen);
726
- p = 0;
727
- writeUInt32BE(packet, sesLen, p);
728
- packet.set(sessionID, p += 4);
729
- p += sesLen;
730
- } else {
731
- packet = this._packetRW.write.alloc(payloadLen);
732
- p = this._packetRW.write.allocStart;
733
- }
734
-
735
- packet[p] = MESSAGE.USERAUTH_REQUEST;
736
-
737
- writeUInt32BE(packet, userLen, ++p);
738
- packet.utf8Write(username, p += 4, userLen);
739
-
740
- writeUInt32BE(packet, 14, p += userLen);
741
- packet.utf8Write('ssh-connection', p += 4, 14);
742
-
743
- writeUInt32BE(packet, 9, p += 14);
744
- packet.utf8Write('publickey', p += 4, 9);
745
-
746
- packet[p += 9] = (cbSign ? 1 : 0);
747
-
748
- writeUInt32BE(packet, algoLen, ++p);
749
- packet.utf8Write(keyAlgo, p += 4, algoLen);
738
+ this._authPKCommon(username, keyAlgo, pubKeyData, signAlgo, cbSign);
739
+ }
750
740
 
751
- writeUInt32BE(packet, pubKeyLen, p += algoLen);
752
- packet.set(pubKeyData, p += 4);
741
+ _authPKCommon(username, algo, pubKey, signAlgo, cbSign) {
742
+ const userLen = Buffer.byteLength(username);
743
+ const algoLen = Buffer.byteLength(algo);
744
+ const pubKeyLen = pubKey.length;
745
+
746
+ // Calculate payload length for the basic packet
747
+ // Header: MSG_USERAUTH_REQUEST (1) + user (4+len) + service (4+14) + method (4+9) + sign_flag (1) + algo (4+len) + key (4+len)
748
+ const basePayloadLen = 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen;
753
749
 
754
750
  if (!cbSign) {
755
- this._authsQueue.push('publickey');
751
+ // -----------------------------------------------------------------------
752
+ // Send "Check" Packet (Signature Flag = 0)
753
+ // -----------------------------------------------------------------------
754
+ const packet = this._packetRW.write.alloc(basePayloadLen);
755
+ let p = this._packetRW.write.allocStart;
756
756
 
757
- this._debug && this._debug(
758
- 'Outbound: Sending USERAUTH_REQUEST (publickey -- check)'
759
- );
760
- sendPacket(this, this._packetRW.write.finalize(packet));
761
- return;
762
- }
763
-
764
- cbSign(packet, (signature) => {
765
- signature = convertSignature(signature, signAlgo);
766
- if (signature === false)
767
- throw new Error('Error while converting handshake signature');
768
-
769
- const sigLen = signature.length;
770
- p = this._packetRW.write.allocStart;
771
- packet = this._packetRW.write.alloc(
772
- 1 + 4 + userLen + 4 + 14 + 4 + 9 + 1 + 4 + algoLen + 4 + pubKeyLen + 4
773
- + 4 + signAlgoLen + 4 + sigLen
774
- );
775
-
776
- // TODO: simply copy from original "packet" to new `packet` to avoid
777
- // having to write each individual field a second time?
778
757
  packet[p] = MESSAGE.USERAUTH_REQUEST;
779
-
780
758
  writeUInt32BE(packet, userLen, ++p);
781
759
  packet.utf8Write(username, p += 4, userLen);
782
-
783
760
  writeUInt32BE(packet, 14, p += userLen);
784
761
  packet.utf8Write('ssh-connection', p += 4, 14);
785
-
786
762
  writeUInt32BE(packet, 9, p += 14);
787
763
  packet.utf8Write('publickey', p += 4, 9);
788
-
789
- packet[p += 9] = 1;
790
-
764
+ packet[p += 9] = 0; // Not signed
791
765
  writeUInt32BE(packet, algoLen, ++p);
792
- packet.utf8Write(keyAlgo, p += 4, algoLen);
793
-
766
+ packet.utf8Write(algo, p += 4, algoLen);
794
767
  writeUInt32BE(packet, pubKeyLen, p += algoLen);
795
- packet.set(pubKeyData, p += 4);
768
+ packet.set(pubKey, p += 4);
796
769
 
797
- // Signature blob: length-prefixed (algorithm string + raw signature)
798
- // For RSA signatures, use the base signing algorithm (without cert suffix)
799
- // The cert suffix is only used in the public key algorithm field of the main packet
800
- writeUInt32BE(packet, 4 + signAlgoLen + 4 + sigLen, p += pubKeyLen);
770
+ this._authsQueue.push('publickey');
771
+ this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (publickey -- check)');
772
+ sendPacket(this, this._packetRW.write.finalize(packet));
773
+ return;
774
+ }
801
775
 
776
+ // -------------------------------------------------------------------------
777
+ // Signing Process
778
+ // -------------------------------------------------------------------------
779
+ const sessionID = this._kex.sessionID;
780
+ const sesLen = sessionID.length;
781
+
782
+ // Construct the data payload to be signed: SessionID + UserAuth Request Packet
783
+ // (Note: The packet construction here mimics the one above but sets sign_flag = 1)
784
+ const dataToSignLen = 4 + sesLen + basePayloadLen;
785
+ const dataToSign = Buffer.allocUnsafe(dataToSignLen);
786
+ let p = 0;
787
+
788
+ writeUInt32BE(dataToSign, sesLen, p);
789
+ dataToSign.set(sessionID, p += 4);
790
+ p += sesLen;
791
+
792
+ dataToSign[p] = MESSAGE.USERAUTH_REQUEST;
793
+ writeUInt32BE(dataToSign, userLen, ++p);
794
+ dataToSign.utf8Write(username, p += 4, userLen);
795
+ writeUInt32BE(dataToSign, 14, p += userLen);
796
+ dataToSign.utf8Write('ssh-connection', p += 4, 14);
797
+ writeUInt32BE(dataToSign, 9, p += 14);
798
+ dataToSign.utf8Write('publickey', p += 4, 9);
799
+ dataToSign[p += 9] = 1; // Signed
800
+ writeUInt32BE(dataToSign, algoLen, ++p);
801
+ dataToSign.utf8Write(algo, p += 4, algoLen);
802
+ writeUInt32BE(dataToSign, pubKeyLen, p += algoLen);
803
+ dataToSign.set(pubKey, p += 4);
804
+
805
+ cbSign(dataToSign, (signature) => {
806
+ signature = convertSignature(signature, signAlgo);
807
+ if (signature === false)
808
+ throw new Error('Error while converting handshake signature');
809
+
810
+ const sigLen = signature.length;
811
+ const signAlgoLen = Buffer.byteLength(signAlgo);
812
+
813
+ // -----------------------------------------------------------------------
814
+ // Send Signed Packet
815
+ // -----------------------------------------------------------------------
816
+ // We reconstruct the packet to send. It is identical to the data part of
817
+ // dataToSign (minus SessionID), plus the signature blob appended at the end.
818
+
819
+ const totalPacketLen = basePayloadLen + 4 + 4 + signAlgoLen + 4 + sigLen;
820
+ p = this._packetRW.write.allocStart;
821
+ const packet = this._packetRW.write.alloc(totalPacketLen);
822
+
823
+ // Copy the standard fields from our signing buffer (skipping sessionID)
824
+ // Offset of packet start in dataToSign is 4 + sesLen
825
+ const packetStartInSigData = 4 + sesLen;
826
+ bufferCopy(dataToSign, packet, packetStartInSigData, dataToSign.length, p);
827
+
828
+ // Move pointer to end of copied data
829
+ p += (dataToSign.length - packetStartInSigData);
830
+
831
+ // Append Signature Blob
832
+ // Length of signature structure (string len + string + blob len + blob)
833
+ writeUInt32BE(packet, 4 + signAlgoLen + 4 + sigLen, p);
834
+
802
835
  writeUInt32BE(packet, signAlgoLen, p += 4);
803
836
  packet.utf8Write(signAlgo, p += 4, signAlgoLen);
804
-
837
+
805
838
  writeUInt32BE(packet, sigLen, p += signAlgoLen);
806
839
  packet.set(signature, p += 4);
807
840
 
808
- // Servers shouldn't send packet type 60 in response to signed publickey
809
- // attempts, but if they do, interpret as type 60.
810
841
  this._authsQueue.push('publickey');
811
-
812
- this._debug && this._debug(
813
- 'Outbound: Sending USERAUTH_REQUEST (publickey)'
814
- );
842
+ this._debug && this._debug('Outbound: Sending USERAUTH_REQUEST (publickey)');
815
843
  sendPacket(this, this._packetRW.write.finalize(packet));
816
844
  });
817
845
  }
@@ -8,6 +8,7 @@ const {
8
8
  Writable: WritableStream
9
9
  } = require('stream');
10
10
  const { inherits, types: { isDate } } = require('util');
11
+ const iconv = require('iconv-lite');
11
12
 
12
13
  const FastBuffer = Buffer[Symbol.species];
13
14
 
@@ -147,6 +148,7 @@ class SFTP extends EventEmitter {
147
148
  this._version = -1;
148
149
  this._extensions = {};
149
150
  this._biOpt = cfg.biOpt;
151
+ this._encoding = cfg.encoding || 'utf8';
150
152
  this._pktLenBytes = 0;
151
153
  this._pktLen = 0;
152
154
  this._pktPos = 0;
@@ -2949,15 +2951,14 @@ const CLIENT_HANDLERS = {
2949
2951
  if (count !== undefined) {
2950
2952
  let names = [];
2951
2953
  for (let i = 0; i < count; ++i) {
2952
- // We are going to assume UTF-8 for filenames despite the SFTPv3
2953
- // spec not specifying an encoding because the specs for newer
2954
- // versions of the protocol all explicitly specify UTF-8 for
2955
- // filenames
2956
- const filename = bufferParser.readString(true);
2954
+ // Decode filenames using the configured encoding
2955
+ const filenameBuf = bufferParser.readString();
2956
+ const filename = (filenameBuf ? iconv.decode(filenameBuf, sftp._encoding) : '');
2957
2957
 
2958
2958
  // `longname` only exists in SFTPv3 and since it typically will
2959
- // contain the filename, we assume it is also UTF-8
2960
- const longname = bufferParser.readString(true);
2959
+ // contain the filename, we assume it uses the same encoding
2960
+ const longnameBuf = bufferParser.readString();
2961
+ const longname = (longnameBuf ? iconv.decode(longnameBuf, sftp._encoding) : '');
2961
2962
 
2962
2963
  const attrs = readAttrs(sftp._biOpt);
2963
2964
  if (attrs === undefined) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electerm/ssh2",
3
- "version": "1.17.1",
3
+ "version": "1.18.0",
4
4
  "author": "Brian White <mscdex@mscdex.net>",
5
5
  "description": "SSH2 client and server modules written in pure JavaScript for node.js",
6
6
  "main": "./lib/index.js",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "asn1": "^0.2.6",
12
- "bcrypt-pbkdf": "^1.0.2"
12
+ "bcrypt-pbkdf": "^1.0.2",
13
+ "iconv-lite": "^0.7.2"
13
14
  },
14
15
  "devDependencies": {
15
16
  "@mscdex/eslint-config": "^1.1.0",