Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Col encrypt support #1667

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/always-encrypted/cek-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,21 @@ export class CEKEntry {
throw new Error('Invalid databaseId, cekId, cekVersion or cekMdVersion.');
}
}

addEntry(encryptionKey: EncryptionKeyInfo): void {
this.columnEncryptionKeyValues.push(encryptionKey);

if (this.databaseId === 0) {
this.databaseId = encryptionKey.dbId;
this.cekId = encryptionKey.keyId;
this.cekVersion = encryptionKey.keyVersion;
this.cekMdVersion = encryptionKey.mdVersion;
} else if ((this.databaseId !== encryptionKey.dbId) ||
(this.cekId !== encryptionKey.keyId) ||
(this.cekVersion !== encryptionKey.keyVersion) ||
!this.cekMdVersion || !encryptionKey.mdVersion ||
this.cekMdVersion.length !== encryptionKey.mdVersion.length) {
throw new Error('Invalid databaseId, cekId, cekVersion or cekMdVersion.');
}
}
}
14 changes: 7 additions & 7 deletions src/always-encrypted/key-crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Copyright (c) 2019 Microsoft Corporation

import { type CryptoMetadata, type EncryptionKeyInfo } from './types';
import { type InternalConnectionOptions as ConnectionOptions } from '../connection';
import { type ParserOptions } from '../token/stream-parser';
import SymmetricKey from './symmetric-key';
import { getKey } from './symmetric-key-cache';
import { AeadAes256CbcHmac256Algorithm, algorithmName } from './aead-aes-256-cbc-hmac-algorithm';
Expand All @@ -16,7 +16,7 @@ export const validateAndGetEncryptionAlgorithmName = (cipherAlgorithmId: number,
return algorithmName;
};

export const encryptWithKey = async (plaintext: Buffer, md: CryptoMetadata, options: ConnectionOptions): Promise<Buffer> => {
export const encryptWithKey = async (plaintext: Buffer, md: CryptoMetadata, options: ParserOptions): Promise<Buffer> => {
if (!options.trustedServerNameAE) {
throw new Error('Server name should not be null in EncryptWithKey');
}
Expand All @@ -38,14 +38,14 @@ export const encryptWithKey = async (plaintext: Buffer, md: CryptoMetadata, opti
return cipherText;
};

export const decryptWithKey = (cipherText: Buffer, md: CryptoMetadata, options: ConnectionOptions): Buffer => {
export const decryptWithKey = async (cipherText: Buffer, md: CryptoMetadata, options: ParserOptions): Promise<Buffer> => {
if (!options.trustedServerNameAE) {
throw new Error('Server name should not be null in DecryptWithKey');
}

// if (!md.cipherAlgorithm) {
// await decryptSymmetricKey(md, options);
// }
if (!md.cipherAlgorithm) {
await decryptSymmetricKey(md, options);
}

if (!md.cipherAlgorithm) {
throw new Error('Cipher Algorithm should not be null in DecryptWithKey');
Expand All @@ -60,7 +60,7 @@ export const decryptWithKey = (cipherText: Buffer, md: CryptoMetadata, options:
return plainText;
};

export const decryptSymmetricKey = async (md: CryptoMetadata, options: ConnectionOptions): Promise<void> => {
export const decryptSymmetricKey = async (md: CryptoMetadata, options: ParserOptions): Promise<void> => {
if (!md) {
throw new Error('md should not be null in DecryptSymmetricKey.');
}
Expand Down
4 changes: 2 additions & 2 deletions src/always-encrypted/symmetric-key-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@

import { type EncryptionKeyInfo } from './types';
import SymmetricKey from './symmetric-key';
import { type InternalConnectionOptions as ConnectionOptions } from '../connection';
import { type ParserOptions } from '../token/stream-parser';
import LRU from 'lru-cache';

const cache = new LRU<string, SymmetricKey>(0);

export const getKey = async (keyInfo: EncryptionKeyInfo, options: ConnectionOptions): Promise<SymmetricKey> => {
export const getKey = async (keyInfo: EncryptionKeyInfo, options: ParserOptions): Promise<SymmetricKey> => {
if (!options.trustedServerNameAE) {
throw new Error('Server name should not be null in getKey');
}
Expand Down
2 changes: 1 addition & 1 deletion src/always-encrypted/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface EncryptionAlgorithm {
}

export interface CryptoMetadata {
cekEntry?: CEKEntry;
cekEntry?: CEKEntry | undefined;
cipherAlgorithmId: number;
cipherAlgorithmName?: string;
normalizationRuleVersion: Buffer;
Expand Down
99 changes: 98 additions & 1 deletion src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import Message from './message';
import { type Metadata } from './metadata-parser';
import { createNTLMRequest } from './ntlm';
import { ColumnEncryptionAzureKeyVaultProvider } from './always-encrypted/keystore-provider-azure-key-vault';
import { shouldHonorAE } from './always-encrypted/utils';
import { getParameterEncryptionMetadata } from './always-encrypted/get-parameter-encryption-metadata';

import { type Parameter, TYPES } from './data-type';
import { BulkLoadPayload } from './bulk-load-payload';
Expand Down Expand Up @@ -397,6 +399,11 @@ interface KeyStoreProviderMap {
[key: string]: ColumnEncryptionAzureKeyVaultProvider;
}

interface KeyStoreProvider {
key: string;
value: ColumnEncryptionAzureKeyVaultProvider;
}

/**
* @private
*/
Expand Down Expand Up @@ -520,6 +527,10 @@ export interface ConnectionOptions {
*/
cancelTimeout?: number;

columnEncryptionKeyCacheTTL?: number;

columnEncryptionSetting?: boolean;

/**
* A function with parameters `(columnName, index, columnMetaData)` and returning a string. If provided,
* this will be called once per column per result-set. The returned value will be used instead of the SQL-provided
Expand Down Expand Up @@ -675,6 +686,8 @@ export interface ConnectionOptions {
*/
encrypt?: string | boolean;

encryptionKeyStoreProviders?: KeyStoreProvider[];

/**
* By default, if the database requested by [[database]] cannot be accessed,
* the connection will fail with an error. However, if [[fallbackToDefaultDb]] is
Expand Down Expand Up @@ -871,6 +884,7 @@ interface RoutingData {
port: number;
}


/**
* A [[Connection]] instance represents a single connection to a database server.
*
Expand Down Expand Up @@ -1690,6 +1704,26 @@ class Connection extends EventEmitter {
this.config.options.useUTC = config.options.useUTC;
}

if (config.options.columnEncryptionSetting !== undefined) {
if (typeof config.options.columnEncryptionSetting !== 'boolean') {
throw new TypeError('The "config.options.columnEncryptionSetting" property must be of type boolean.');
}

this.config.options.columnEncryptionSetting = config.options.columnEncryptionSetting;
}

if (config.options.columnEncryptionKeyCacheTTL !== undefined) {
if (typeof config.options.columnEncryptionKeyCacheTTL !== 'number') {
throw new TypeError('The "config.options.columnEncryptionKeyCacheTTL" property must be of type number.');
}

if (config.options.columnEncryptionKeyCacheTTL <= 0) {
throw new TypeError('The "config.options.columnEncryptionKeyCacheTTL" property must be greater than 0.');
}

this.config.options.columnEncryptionKeyCacheTTL = config.options.columnEncryptionKeyCacheTTL;
}

if (config.options.workstationId !== undefined) {
if (typeof config.options.workstationId !== 'string') {
throw new TypeError('The "config.options.workstationId" property must be of type string.');
Expand All @@ -1705,6 +1739,51 @@ class Connection extends EventEmitter {

this.config.options.lowerCaseGuids = config.options.lowerCaseGuids;
}

if (config.options.encryptionKeyStoreProviders) {
for (const entry of config.options.encryptionKeyStoreProviders) {
const providerName = entry.key;

if (!providerName || providerName.length === 0) {
throw new TypeError('Invalid key store provider name specified. Key store provider names cannot be null or empty.');
}

if (providerName.substring(0, 6).toUpperCase().localeCompare('MSSQL_') === 0) {
throw new TypeError(`Invalid key store provider name ${providerName}. MSSQL_ prefix is reserved for system key store providers.`);
}

if (!entry.value) {
throw new TypeError(`Null reference specified for key store provider ${providerName}. Expecting a non-null value.`);
}

if (!this.config.options.encryptionKeyStoreProviders) {
this.config.options.encryptionKeyStoreProviders = {};
}

this.config.options.encryptionKeyStoreProviders[providerName] = entry.value;
}
}
}

let serverName = this.config.server;
if (!serverName) {
serverName = 'localhost';
}

const px = serverName.indexOf('\\');

if (px > 0) {
serverName = serverName.substring(0, px);
}

this.config.options.trustedServerNameAE = serverName;

if (this.config.options.instanceName) {
this.config.options.trustedServerNameAE = `${this.config.options.trustedServerNameAE}:${this.config.options.instanceName}`;
}

if (this.config.options.port) {
this.config.options.trustedServerNameAE = `${this.config.options.trustedServerNameAE}:${this.config.options.port}`;
}

this.secureContextOptions = this.config.options.cryptoCredentialsDetails;
Expand Down Expand Up @@ -2592,7 +2671,7 @@ class Connection extends EventEmitter {
*
* @param request A [[Request]] object representing the request.
*/
execSql(request: Request) {
_execSql(request: Request) {
try {
request.validateParameters(this.databaseCollation);
} catch (error: any) {
Expand Down Expand Up @@ -2635,6 +2714,24 @@ class Connection extends EventEmitter {
this.makeRequest(request, TYPE.RPC_REQUEST, new RpcRequestPayload(Procedures.Sp_ExecuteSql, parameters, this.currentTransactionDescriptor(), this.config.options, this.databaseCollation));
}

execSql(request: Request) {
request.shouldHonorAE = shouldHonorAE(request.statementColumnEncryptionSetting, this.config.options.columnEncryptionSetting);
if (request.shouldHonorAE && request.cryptoMetadataLoaded === false && (request.parameters && request.parameters.length > 0)) {
getParameterEncryptionMetadata(this, request, (error?: Error) => {
if (error != null) {
process.nextTick(() => {
this.transitionTo(this.STATE.LOGGED_IN);
this.debug.log(error.message);
request.callback(error);
});
return;
}
this._execSql(request);
});
} else {
this._execSql(request);
}
}
/**
* Creates a new BulkLoad instance.
*
Expand Down
20 changes: 19 additions & 1 deletion src/login7-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,20 @@ const FEDAUTH_OPTIONS = {

const FEATURE_EXT_TERMINATOR = 0xFF;

const COLUMN_ENCRYPTION_OPTIONS = {
FEATURE_ID: 0x04,
MAX_SUPPORTED_CRYPTO_VERSION: 0x01
};

interface Options {
tdsVersion: number;
packetSize: number;
clientProgVer: number;
clientPid: number;
connectionId: number;
clientTimeZone: number;
// Depercated
// The ClientLCID value is no longer used to set language parameters and is ignored.
clientLcid: number;
}

Expand Down Expand Up @@ -103,7 +110,7 @@ class Login7Payload {
declare changePassword: string | undefined;

declare fedAuth: { type: 'ADAL', echo: boolean, workflow: 'default' | 'integrated' } | { type: 'SECURITYTOKEN', echo: boolean, fedAuthToken: string } | undefined;

declare columnEncryption: boolean;
constructor({ tdsVersion, packetSize, clientProgVer, clientPid, connectionId, clientTimeZone, clientLcid }: Options) {
this.tdsVersion = tdsVersion;
this.packetSize = packetSize;
Expand All @@ -117,6 +124,7 @@ class Login7Payload {
this.initDbFatal = false;

this.fedAuth = undefined;
this.columnEncryption = false;

this.userName = undefined;
this.password = undefined;
Expand Down Expand Up @@ -412,6 +420,16 @@ class Login7Payload {
}
}

if (this.columnEncryption) {
const buffer = Buffer.alloc(6);
let offset = 0;
offset = buffer.writeUInt8(COLUMN_ENCRYPTION_OPTIONS.FEATURE_ID, offset);
offset = buffer.writeUInt32LE(1, offset);
buffer.writeUInt8(COLUMN_ENCRYPTION_OPTIONS.MAX_SUPPORTED_CRYPTO_VERSION, offset);

buffers.push(buffer);
}

if (this.tdsVersion >= versions['7_4']) {
// Signal UTF-8 support: Value 0x0A, bit 0 must be set to 1. Added in TDS 7.4.
const UTF8_SUPPORT_FEATURE_ID = 0x0a;
Expand Down
10 changes: 5 additions & 5 deletions src/metadata-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export type BaseMetadata = {
}

export type Metadata = {
cryptoMetadata?: CryptoMetadata;
cryptoMetadata?: CryptoMetadata | undefined;
} & BaseMetadata;

function readCollation(buf: Buffer, offset: number): Result<Collation> {
Expand Down Expand Up @@ -113,12 +113,12 @@ function readUDTInfo(buf: Buffer, offset: number): Result<UdtInfo> {
}, offset);
}

function readMetadata(buf: Buffer, offset: number, options: ParserOptions): Result<Metadata> {
function readMetadata(buf: Buffer, offset: number, options: ParserOptions, shouldReadFlags: boolean): Result<Metadata> {
let userType;
({ offset, value: userType } = (options.tdsVersion < '7_2' ? readUInt16LE : readUInt32LE)(buf, offset));

let flags;
({ offset, value: flags } = readUInt16LE(buf, offset));
shouldReadFlags ? ({ offset, value: flags } = readUInt16LE(buf, offset)) : flags = 0;

let typeNumber;
({ offset, value: typeNumber } = readUInt8(buf, offset));
Expand Down Expand Up @@ -354,12 +354,12 @@ function readMetadata(buf: Buffer, offset: number, options: ParserOptions): Resu
}
}

function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void) {
function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void, shouldReadFlags = true) {
(async () => {
while (true) {
let result;
try {
result = readMetadata(parser.buffer, parser.position, options);
result = readMetadata(parser.buffer, parser.position, options, shouldReadFlags);
} catch (err: any) {
if (err instanceof NotEnoughDataError) {
await parser.waitForChunk();
Expand Down
7 changes: 5 additions & 2 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface ParameterOptions {
length?: number;
precision?: number;
scale?: number;

forceEncrypt?: boolean;
}

interface RequestOptions {
Expand Down Expand Up @@ -399,7 +401,7 @@ class Request extends EventEmitter {
*/
// TODO: `type` must be a valid TDS value type
addParameter(name: string, type: DataType, value?: unknown, options?: Readonly<ParameterOptions> | null) {
const { output = false, length, precision, scale } = options ?? {};
const { output = false, length, precision, scale, forceEncrypt = false } = options ?? {};

const parameter: Parameter = {
type: type,
Expand All @@ -408,7 +410,8 @@ class Request extends EventEmitter {
output: output,
length: length,
precision: precision,
scale: scale
scale: scale,
forceEncrypt: forceEncrypt
};

this.parameters.push(parameter);
Expand Down
Loading
Loading