Skip to content

feat: encrypt the login packet when encrypt is false - #1825

Open
arthurschreiber wants to merge 2 commits into
masterfrom
arthur/login-only-encryption
Open

arthurschreiber wants to merge 2 commits into
masterfrom
arthur/login-only-encryption

Conversation

@arthurschreiber

Copy link
Copy Markdown
Collaborator

Closes #1822.

When encrypt is false, tedious advertised ENCRYPT_NOT_SUP in PRELOGIN, so the LOGIN7 packet (including the SQL authentication password, which is only obfuscated) was always sent unencrypted, even when the server supports TLS.

This changes tedious to match the login-only encryption behavior of SqlClient, JDBC and ODBC:

  • encrypt: false now advertises ENCRYPT_OFF.
  • If the server responds with ENCRYPT_OFF, a TLS handshake is performed and the first packet of the LOGIN7 message is sent over TLS. The TLS session is then discarded and the connection continues unencrypted. Encrypting only the first packet is what SqlClient does (TdsParserStateObject.cs), and SQL Server rejects or hangs on any other split.
  • ENCRYPT_NOT_SUP, ENCRYPT_ON / ENCRYPT_REQ and encrypt: 'strict' behave as before.

Behavior notes

These are documented on the encrypt and trustServerCertificate options:

  • The server certificate is not validated for login-only encryption, regardless of trustServerCertificate. This protects credentials against passive eavesdropping, not against an active man-in-the-middle.
  • Only the first packet is encrypted. With a packetSize of 4096 (default) or more, this always covers the SQL authentication password. Access tokens and other authentication data are not covered.
  • If the TLS handshake fails, the connection fails with Failed to connect to <server> - TLS handshake to encrypt the login packet failed: <reason> (ESOCKET). Servers that only support old TLS versions can be allowed via cryptoCredentialsDetails (e.g. { minVersion: 'TLSv1' }).
  • The secure event is not emitted for login-only encryption.

Performance

No change for the query path; sendMessage and the post-login socket piping are untouched.

Connection setup for encrypt: false now includes a TLS handshake. Against SQL Server 2025 on Linux (local Docker), mean connect time went from 74 ms to 100 ms, the same as encrypt: true, which is unchanged. Tracing shows almost all of this is the server's response time to the TLS ClientHello (~25 ms); the client-side overhead is below 1 ms.

Tests

  • Unit tests with a loopback server that performs the server side of login-only encryption: first packet arrives as a single TLS record, remaining packets are plaintext, no credentials on the wire, untrusted certificates are accepted, handshake failures produce the new error, multi-packet LOGIN7 with packetSize: 512.
  • Integration tests for encrypt: false with packetSize 4096 and 512, verifying login-only TLS was negotiated and encrypt_option is FALSE.

🤖 Generated with Claude Code

arthurschreiber and others added 2 commits October 11, 2026 12:00
When `encrypt` is `false`, tedious now advertises `ENCRYPT_OFF` instead of
`ENCRYPT_NOT_SUP` in PRELOGIN. If the server supports encryption and responds
with `ENCRYPT_OFF`, the first packet of the LOGIN7 message is sent over TLS,
and the rest of the connection stays unencrypted, matching the login-only
encryption behavior of other SQL Server drivers.

The server certificate is not validated for login-only encryption. If the TLS
handshake fails, the connection fails with an error explaining that the login
packet could not be encrypted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@arthurschreiber
arthurschreiber force-pushed the arthur/login-only-encryption branch from 4b2d82e to 43992ca Compare October 11, 2026 12:00
@codecov

codecov Bot commented Oct 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.84%. Comparing base (b10a088) to head (43992ca).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/message-io.ts 88.88% 2 Missing and 1 partial ⚠️
src/connection.ts 83.33% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1825      +/-   ##
==========================================
+ Coverage   84.76%   84.84%   +0.07%     
==========================================
  Files          96       96              
  Lines        4720     4751      +31     
  Branches      884      889       +5     
==========================================
+ Hits         4001     4031      +30     
  Misses        455      455              
- Partials      264      265       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cheenamalhotra cheenamalhotra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. A few hardening suggestions inline.

Follow-up idea (not blocking): when we send ENCRYPT_OFF and the server replies ENCRYPT_REQ, SqlClient upgrades to full encryption, while we still throw EENCRYPT (Server requires encryption...). Could be a separate PR.

Comment thread src/message-io.ts
this.outgoingMessageStream.pause();

securePair.encrypted.on('data', forwardEncrypted);
securePair.cleartext.write(packet, (err) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This relies on the encrypted bytes reaching forwardEncrypted before the write callback runs. If they arrive later, the listener is already removed and the session destroyed, so the login packet is lost and the connection hangs. Tests pass, but it depends on Node/native-duplexpair timing.

Safer: drain what is left before destroying, e.g.

let chunk;
while ((chunk = securePair.encrypted.read()) !== null) {
  this.socket.write(chunk);
}

Comment thread src/message-io.ts
};

this.outgoingMessageStream.unpipe(this.socket);
this.outgoingMessageStream.once('data', (packet: Buffer) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the connection is aborted or times out after the handshake but before LOGIN7 is sent, this once('data') listener and the TLS session are never cleaned up. Suggest tearing them down on socket close as well.

Comment thread src/message-io.ts
private encryptNextPacket(securePair: { cleartext: tls.TLSSocket, encrypted: Duplex }, onSecureError: (err: Error) => void) {
// The TLS record size must not be capped below the packet size, as the
// server expects the encrypted packet to arrive in a single TLS record.
securePair.cleartext.setMaxSendFragment(MessageIO.MAX_TLS_SEND_FRAGMENT_SIZE);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

packetSize can be up to 32767, but a TLS record is max 16 KB. If the first LOGIN7 packet is larger than that, it is split into two records, which the server rejects. Maybe cap the first packet or document the limit.

done();
});

request.on('row', function(columns) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asserts inside the row handler throw outside done, so a failure shows up as an uncaught error. Suggest collecting the values here and asserting them in the request callback.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE REQUEST] Encrypt the login packet when encrypt is false and the server supports TLS

2 participants