diff --git a/src/always-encrypted/cek-entry.ts b/src/always-encrypted/cek-entry.ts index e25cc6387..586db4af7 100644 --- a/src/always-encrypted/cek-entry.ts +++ b/src/always-encrypted/cek-entry.ts @@ -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.'); + } + } } diff --git a/src/always-encrypted/types.ts b/src/always-encrypted/types.ts index b7bd236d9..adbc8a467 100644 --- a/src/always-encrypted/types.ts +++ b/src/always-encrypted/types.ts @@ -27,7 +27,7 @@ export interface EncryptionAlgorithm { } export interface CryptoMetadata { - cekEntry?: CEKEntry; + cekEntry?: CEKEntry | undefined; cipherAlgorithmId: number; cipherAlgorithmName?: string; normalizationRuleVersion: Buffer; diff --git a/src/login7-payload.ts b/src/login7-payload.ts index 01b85f94c..0f9782edf 100644 --- a/src/login7-payload.ts +++ b/src/login7-payload.ts @@ -64,6 +64,11 @@ 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; @@ -71,6 +76,8 @@ interface Options { clientPid: number; connectionId: number; clientTimeZone: number; + // Depercated + // The ClientLCID value is no longer used to set language parameters and is ignored. clientLcid: number; } @@ -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; @@ -117,6 +124,7 @@ class Login7Payload { this.initDbFatal = false; this.fedAuth = undefined; + this.columnEncryption = false; this.userName = undefined; this.password = undefined; @@ -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; diff --git a/src/metadata-parser.ts b/src/metadata-parser.ts index 8dc33e801..408cb89d1 100644 --- a/src/metadata-parser.ts +++ b/src/metadata-parser.ts @@ -52,7 +52,7 @@ export type BaseMetadata = { } export type Metadata = { - cryptoMetadata?: CryptoMetadata; + cryptoMetadata?: CryptoMetadata | undefined; } & BaseMetadata; function readCollation(buf: Buffer, offset: number): Result { @@ -113,12 +113,12 @@ function readUDTInfo(buf: Buffer, offset: number): Result { }, offset); } -function readMetadata(buf: Buffer, offset: number, options: ParserOptions): Result { +function readMetadata(buf: Buffer, offset: number, options: ParserOptions, shouldReadFlags: boolean): Result { 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)); @@ -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(); diff --git a/src/token/colmetadata-token-parser.ts b/src/token/colmetadata-token-parser.ts index 1b6d111e1..7951c973f 100644 --- a/src/token/colmetadata-token-parser.ts +++ b/src/token/colmetadata-token-parser.ts @@ -1,8 +1,9 @@ import { readMetadata, type Metadata } from '../metadata-parser'; - +import { CEKEntry } from '../always-encrypted/cek-entry'; +import { type CryptoMetadata, type EncryptionKeyInfo } from '../always-encrypted/types'; import Parser, { type ParserOptions } from './stream-parser'; import { ColMetadataToken } from './token'; -import { NotEnoughDataError, Result, readBVarChar, readUInt16LE, readUInt8, readUsVarChar } from './helpers'; +import { NotEnoughDataError, Result, readBVarChar, readUInt16LE, readUInt8, readUsVarChar, readUInt16BE, readUInt32LE } from './helpers'; export interface ColumnMetadata extends Metadata { /** @@ -13,6 +14,13 @@ export interface ColumnMetadata extends Metadata { tableName?: string | string[] | undefined; } +type cekTableEntryMetadata = { + databaseId: number; + cekId: number; + cekVersion: number; + cekMdVersion: Buffer; +} + function readTableName(buf: Buffer, offset: number, metadata: Metadata, options: ParserOptions): Result { if (!metadata.type.hasTableName) { return new Result(undefined, offset); @@ -51,13 +59,199 @@ function readColumnName(buf: Buffer, offset: number, index: number, metadata: Me } } -function readColumn(buf: Buffer, offset: number, options: ParserOptions, index: number) { +async function readCEKTable(parser: Parser): Promise> { + + let tableSize; + + while (true) { + let offset; + + try { + ({ offset, value: tableSize } = readUInt16LE(parser.buffer, parser.position)); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + break; + } + if (tableSize > 0) { + const cekEntries: CEKEntry[] = []; + for (let i = 0; i < tableSize; i++) { + while (true) { + let cek: CEKEntry; + let offset; + try { + ({ offset, value: cek } = await readCEKTableEntry(parser)); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + cekEntries.push(cek); + + break; + } + } + return new Result(cekEntries, parser.position); + } + return new Result(undefined, parser.position); +} + + +async function readCEKTableEntry(parser: Parser): Promise> { + let databaseId; + let cekId; + let cekVersion; + let cekMdVersion; + let cekValueCount; + + while (true) { + let offset = parser.position; + try { + ({ offset, value: databaseId } = readUInt32LE(parser.buffer, offset)); + ({ offset, value: cekId } = readUInt32LE(parser.buffer, offset)); + ({ offset, value: cekVersion } = readUInt32LE(parser.buffer, offset)); + cekMdVersion = parser.buffer.subarray(offset, offset + 8); + ({ offset, value: cekValueCount } = readUInt8(parser.buffer, offset + 8)); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + break; + } + + const cekEntry = new CEKEntry(cekValueCount); + for (let i = 0; i < cekValueCount; i++) { + while (true) { + let cekValue; + let offset; + try { + ({ offset, value: cekValue } = readCEKValue(parser.buffer, parser.position, { + databaseId: databaseId, + cekId: cekId, + cekVersion: cekVersion, + cekMdVersion: cekMdVersion + })); + } catch (err: any) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + cekEntry.addEntry(cekValue); + + break; + } + } + return new Result(cekEntry, parser.position); +} + +function readCEKValue(buf: Buffer, offset: number, cekTableEntryMetadata: cekTableEntryMetadata,): Result { + let encryptedCEKLength; + ({ offset, value: encryptedCEKLength } = readUInt16LE(buf, offset)); + + const encryptedCEK = buf.subarray(offset, offset + encryptedCEKLength); + + let keyStoreNameLength; + ({ offset, value: keyStoreNameLength } = readUInt8(buf, offset + encryptedCEKLength)); + + const keyStoreName = buf.toString('ucs2', offset, offset + 2 * keyStoreNameLength); + + let keyPathLength; + ({ offset, value: keyPathLength } = readUInt8(buf, offset + 2 * keyStoreNameLength)); + + const keyPath = buf.subarray(offset, offset + 2 * keyPathLength).swap16().toString('ucs2'); + + let algorithmNameLength; + ({ offset, value: algorithmNameLength } = readUInt16BE(buf, offset + 2 * keyPathLength)); + + const algorithmName = buf.toString('ucs2', offset, offset + 2 * algorithmNameLength); + + return new Result({ + encryptedKey: encryptedCEK, + dbId: cekTableEntryMetadata.databaseId, + keyId: cekTableEntryMetadata.cekId, + keyVersion: cekTableEntryMetadata.cekVersion, + mdVersion: cekTableEntryMetadata.cekMdVersion, + keyPath: keyPath, + keyStoreName: keyStoreName, + algorithmName: algorithmName }, offset + 2 * algorithmNameLength); +} + +function readCryptoMetadata(buf: Buffer, offset: number, metadata: Metadata, cekList: CEKEntry[] | undefined, options: ParserOptions): Result { + let ordinal; + cekList ? { offset, value: ordinal } = readUInt16LE(buf, offset) : ordinal = 0; + + ({ offset, value: metadata } = readMetadata(buf, offset, options, false)); + + let algorithmId; + ({ offset, value: algorithmId } = readUInt8(buf, offset)); + + let algorithmName; + ({ offset, value: algorithmName } = readCustomEncryptionMetadata(buf, offset, algorithmId)); + + let encryptionType; + ({ offset, value: encryptionType } = readUInt8(buf, offset)); + + const normalizationRuleVersion = buf.subarray(offset, offset + 1); + + return new Result({ + cekEntry: cekList ? cekList[ordinal] : undefined, + ordinal: ordinal, + cipherAlgorithmId: algorithmId, + cipherAlgorithmName: algorithmName, + encryptionType: encryptionType, + normalizationRuleVersion: normalizationRuleVersion, + baseTypeInfo: metadata }, offset + 1); +} + +function readCustomEncryptionMetadata(buf: Buffer, offset: number, algorithmId: number): Result { + if (algorithmId === 0) { + let nameSize; + ({ offset, value: nameSize } = readUInt8(buf, offset)); + const algorithmName = buf.toString('ucs2', offset, offset + nameSize); + return new Result(algorithmName, offset + nameSize); + } + return new Result('', offset); +} + +function readColumn(buf: Buffer, offset: number, options: ParserOptions, index: number, cekList: CEKEntry[] | undefined): Result { let metadata; - ({ offset, value: metadata } = readMetadata(buf, offset, options)); + ({ offset, value: metadata } = readMetadata(buf, offset, options, true)); let tableName; ({ offset, value: tableName } = readTableName(buf, offset, metadata, options)); + let cryptoMetadata; + if (options.serverSupportsColumnEncryption === true && 0x0800 === (metadata.flags & 0x0800)) { + ({ offset, value: cryptoMetadata } = readCryptoMetadata(buf, offset, metadata, cekList, options)); + if (cryptoMetadata && cryptoMetadata.baseTypeInfo) { + cryptoMetadata.baseTypeInfo.flags = metadata.flags; + metadata.collation = cryptoMetadata.baseTypeInfo.collation; + } + } + let colName; ({ offset, value: colName } = readColumnName(buf, offset, index, metadata, options)); @@ -72,7 +266,8 @@ function readColumn(buf: Buffer, offset: number, options: ParserOptions, index: dataLength: metadata.dataLength, schema: metadata.schema, colName: colName, - tableName: tableName + tableName: tableName, + cryptoMetadata: options.serverSupportsColumnEncryption === true ? cryptoMetadata : undefined, }, offset); } @@ -97,6 +292,27 @@ async function colMetadataParser(parser: Parser): Promise { break; } + let cekList; + if (parser.options.serverSupportsColumnEncryption === true) { + while (true) { + let offset; + + try { + ({ offset, value: cekList } = await readCEKTable(parser)); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + parser.position = offset; + break; + } + } + const columns: ColumnMetadata[] = []; for (let i = 0; i < columnCount; i++) { while (true) { @@ -104,7 +320,7 @@ async function colMetadataParser(parser: Parser): Promise { let offset; try { - ({ offset, value: column } = readColumn(parser.buffer, parser.position, parser.options, i)); + ({ offset, value: column } = readColumn(parser.buffer, parser.position, parser.options, i, cekList)); } catch (err: any) { if (err instanceof NotEnoughDataError) { await parser.waitForChunk(); diff --git a/src/token/feature-ext-ack-parser.ts b/src/token/feature-ext-ack-parser.ts index 0e6a5d034..5ad03a03b 100644 --- a/src/token/feature-ext-ack-parser.ts +++ b/src/token/feature-ext-ack-parser.ts @@ -16,13 +16,14 @@ const FEATURE_ID = { function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOptions): Result { let fedAuth: Buffer | undefined; let utf8Support: boolean | undefined; + let columnEncryption: boolean | undefined; while (true) { let featureId; ({ value: featureId, offset } = readUInt8(buf, offset)); if (featureId === FEATURE_ID.TERMINATOR) { - return new Result(new FeatureExtAckToken(fedAuth, utf8Support), offset); + return new Result(new FeatureExtAckToken(fedAuth, utf8Support, columnEncryption), offset); } let featureAckDataLen; @@ -32,7 +33,7 @@ function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOption throw new NotEnoughDataError(offset + featureAckDataLen); } - const featureData = buf.slice(offset, offset + featureAckDataLen); + const featureData = buf.subarray(offset, offset + featureAckDataLen); offset += featureAckDataLen; switch (featureId) { @@ -42,6 +43,19 @@ function featureExtAckParser(buf: Buffer, offset: number, _options: ParserOption case FEATURE_ID.UTF8_SUPPORT: utf8Support = !!featureData[0]; break; + case FEATURE_ID.COLUMNENCRYPTION: + if (1 > featureData.length) { + throw new Error(`Unsupported featureDataLength ${featureData.length} for feature type ${featureId}`); + } + // TODO: may need to look into support enclave computations version support: + // supportedCryptographicVersion === 0x02 + // supportedCryptographicVersion === 0x03 + const supportedCryptographicVersion = featureData[0]; + if (0 === supportedCryptographicVersion || supportedCryptographicVersion > 0x01) { + throw new Error(`Unsupported supported cryptographic protocol version: ${supportedCryptographicVersion}`); + } + columnEncryption = true; + break; } } } diff --git a/src/token/helpers.ts b/src/token/helpers.ts index c5a1d7137..652da7b3e 100644 --- a/src/token/helpers.ts +++ b/src/token/helpers.ts @@ -38,6 +38,16 @@ export function readUInt16LE(buf: Buffer, offset: number): Result { return new Result(buf.readUInt16LE(offset), offset + 2); } +export function readUInt16BE(buf: Buffer, offset: number): Result { + offset = +offset; + + if (buf.length < offset + 2) { + throw new NotEnoughDataError(offset + 2); + } + + return new Result(buf.readUInt16BE(offset), offset + 2); +} + export function readInt16LE(buf: Buffer, offset: number): Result { offset = +offset; diff --git a/src/token/returnvalue-token-parser.ts b/src/token/returnvalue-token-parser.ts index bcfa14ced..53a520603 100644 --- a/src/token/returnvalue-token-parser.ts +++ b/src/token/returnvalue-token-parser.ts @@ -23,7 +23,7 @@ async function returnParser(parser: Parser): Promise { ({ offset, value: paramName } = readBVarChar(buf, offset)); // status ({ offset } = readUInt8(buf, offset)); - ({ offset, value: metadata } = readMetadata(buf, offset, parser.options)); + ({ offset, value: metadata } = readMetadata(buf, offset, parser.options, true)); if (paramName.charAt(0) === '@') { paramName = paramName.slice(1); diff --git a/src/token/stream-parser.ts b/src/token/stream-parser.ts index abcca5674..6b9a7f516 100644 --- a/src/token/stream-parser.ts +++ b/src/token/stream-parser.ts @@ -18,7 +18,7 @@ import nbcRowParser from './nbcrow-token-parser'; import sspiParser from './sspi-token-parser'; import { NotEnoughDataError } from './helpers'; -export type ParserOptions = Pick; +export type ParserOptions = Pick; class Parser { debug: Debug; diff --git a/src/token/token.ts b/src/token/token.ts index 222d2bf02..14f448d1e 100644 --- a/src/token/token.ts +++ b/src/token/token.ts @@ -332,11 +332,14 @@ export class FeatureExtAckToken extends Token { * undefined when UTF8_SUPPORT not included in token. */ declare utf8Support: boolean | undefined; - constructor(fedAuth: Buffer | undefined, utf8Support: boolean | undefined) { + declare columnEncryption: boolean | undefined; + + constructor(fedAuth: Buffer | undefined, utf8Support: boolean | undefined, columnEncryption: boolean | undefined) { super('FEATUREEXTACK', 'onFeatureExtAck'); this.fedAuth = fedAuth; this.utf8Support = utf8Support; + this.columnEncryption = columnEncryption; } } diff --git a/test/unit/always-encrypted/cek-table-test.js b/test/unit/always-encrypted/cek-table-test.js new file mode 100644 index 000000000..8b63d4953 --- /dev/null +++ b/test/unit/always-encrypted/cek-table-test.js @@ -0,0 +1,71 @@ +const assert = require('chai').assert; +const { CEKEntry } = require('../../../src/always-encrypted/cek-entry'); + +describe('CEKEntry', () => { + it('constructs CEKEntry', () => { + const entry = new CEKEntry(1); + assert.strictEqual(entry.ordinal, 1); + assert.strictEqual(entry.databaseId, 0); + assert.strictEqual(entry.cekId, 0); + assert.strictEqual(entry.cekVersion, 0); + assert.deepEqual(entry.cekMdVersion, Buffer.alloc(0)); + assert.deepEqual(entry.columnEncryptionKeyValues, []); + }); + + it('adds encryption key value', () => { + const entry = new CEKEntry(0); + + entry.add( + Buffer.from([0x01, 0x02, 0x03, 0x04]), + 1, + 1, + 1, + Buffer.from([0x01, 0x01, 0x01]), + 'keyPath', + 'keyStoreName', + 'algorithmName' + ); + + assert.deepEqual(entry.columnEncryptionKeyValues[0], { + encryptedKey: Buffer.from([0x01, 0x02, 0x03, 0x04]), + dbId: 1, + keyId: 1, + keyVersion: 1, + mdVersion: Buffer.from([0x01, 0x01, 0x01]), + keyPath: 'keyPath', + keyStoreName: 'keyStoreName', + algorithmName: 'algorithmName', + }); + assert.strictEqual(entry.ordinal, 0); + assert.strictEqual(entry.databaseId, 1); + assert.strictEqual(entry.cekId, 1); + assert.strictEqual(entry.cekVersion, 1); + assert.deepEqual(entry.cekMdVersion, Buffer.from([0x01, 0x01, 0x01])); + }); + + it('throws when added key metadata does not match other entries', () => { + const entry = new CEKEntry(0); + + entry.add( + Buffer.from([0x01, 0x02, 0x03, 0x04]), + 1, + 1, + 1, + Buffer.from([0x01, 0x01, 0x01]), + 'keyPath', + 'keyStoreName', + 'algorithmName' + ); + + assert.throws(() => entry.add( + Buffer.from([0x01, 0x02, 0x03, 0x04]), + 2, + 1, + 1, + Buffer.from([0x01, 0x01, 0x01]), + 'keyPath', + 'keyStoreName', + 'algorithmName' + ), 'Invalid databaseId, cekId, cekVersion or cekMdVersion.'); + }); +}); diff --git a/test/unit/always-encrypted/crypto-util.js b/test/unit/always-encrypted/crypto-util.js new file mode 100644 index 000000000..075f0de6b --- /dev/null +++ b/test/unit/always-encrypted/crypto-util.js @@ -0,0 +1,180 @@ +const { createCipheriv, createHmac } = require('crypto'); + +const { + deriveKey, + generateKeySalt, +} = require('../../../src/always-encrypted/aead-aes-256-cbc-hmac-encryption-key'); + +const algorithmName = 'AEAD_AES_256_CBC_HMAC_SHA256'; +const keySize = 256; + +// const blockSizeInBytes = 16; +const algorithmVersion = 0x01; +const algorithmVersionSize = 0x01; + +function deriveEncryptionKey(rootKey) { + const salt = generateKeySalt('encryption', algorithmName, keySize); + return deriveKey(rootKey, salt); +} + +function deriveIVKey(rootKey) { + const salt = generateKeySalt('IV', algorithmName, keySize); + return deriveKey(rootKey, salt); +} + +function deriveMACKey(rootKey) { + const salt = generateKeySalt('MAC', algorithmName, keySize); + return deriveKey(rootKey, salt); +} + +function generateCipherText(rootKey, iv, plainText) { + const encryptionKey = deriveEncryptionKey(rootKey); + const cipher = createCipheriv('aes-256-cbc', encryptionKey, iv); + + return Buffer.from([ + ...cipher.update(plainText), + ...cipher.final(), + ]); +} + +function generateAuthenticationTag(rootKey, iv, cipherText) { + const macKey = deriveMACKey(rootKey); + const hmac = createHmac('sha256', macKey); + hmac.update(Buffer.from([algorithmVersion])); + hmac.update(iv); + hmac.update(cipherText); + hmac.update(Buffer.from([algorithmVersionSize])); + return hmac.digest(); +} + +function generateEncryptedVarBinary(rootKey, iv, plainText) { + const cipherText = generateCipherText(rootKey, iv, plainText); + const authenticationTag = generateAuthenticationTag(rootKey, iv, cipherText); + + return Buffer.from([ + algorithmVersion, + ...authenticationTag, + ...iv, + ...cipherText, + ]); +} + +const options = { + useUTC: false, + tdsVersion: '7_2' +}; + +const alwaysEncryptedAlgorithmName = 'AEAD_AES_256_CBC_HMAC_SHA256'; +const alwaysEncryptedCEK = Buffer.from([ + // decrypted column key must be 32 bytes long for AES256 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +]); +const alwaysEncryptedIV = Buffer.from([ + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, + 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, +]); +// pre-calculated constants, mostly for debugging purposes +const alwaysEncryptedConstants = { + // root key is the decrypted column encryption key + // it is arbitrary, but must be 32 bytes long for AES256 + // rootKey: 0000000000000000000000000000000000000000000000000000000000000000 + rootKey: Buffer.from([ ...alwaysEncryptedCEK ]), + + // iv is the initialization vector used when encrypting plaintext + // it is arbitrary, but must be 16 bytes long for AES256 + // iv: 11111111111111111111111111111111 + iv: Buffer.from([ ...alwaysEncryptedIV ]), + + // derived keys must use the root key above, and the appropriate key salt + // format as the input data + // e.g. to get the MAC key, run something like this: + // echo -n \ + // "Microsoft SQL Server cell MAC key with encryption" \ + // "algorithm:AEAD_AES_256_CBC_HMAC_SHA256 and key length:256" | \ + // iconv -f 'UTF-8' -t 'UTF-16LE' | \ + // openssl dgst \ + // -sha256 -mac HMAC \ + // -macopt hexkey:'0000000000000000000000000000000000000000000000000000000000000000' + // # 9d1f2295e509519ed0f1bff77659713280a3651fa2d7a7023abd1ba519012573 + + // encryptionKey: 02c735a87529f1d1eb3853852c2a45cf667331dda269c18feac9aec29675b349 + // encryptionKey: Buffer.from([ + // 0x02, 0xC7, 0x35, 0xA8, 0x75, 0x29, 0xF1, 0xD1, + // 0xEB, 0x38, 0x53, 0x85, 0x2C, 0x2A, 0x45, 0xCF, + // 0x66, 0x73, 0x31, 0xDD, 0xA2, 0x69, 0xC1, 0x8F, + // 0xEA, 0xC9, 0xAE, 0xC2, 0x96, 0x75, 0xB3, 0x49, + // ]), + encryptionKey: deriveEncryptionKey(alwaysEncryptedCEK), + + // macKey: 9d1f2295e509519ed0f1bff77659713280a3651fa2d7a7023abd1ba519012573 + // macKey: Buffer.from([ + // 0x9D, 0x1F, 0x22, 0x95, 0xE5, 0x09, 0x51, 0x9E, + // 0xD0, 0xF1, 0xBF, 0xF7, 0x76, 0x59, 0x71, 0x32, + // 0x80, 0xA3, 0x65, 0x1F, 0xA2, 0xD7, 0xA7, 0x02, + // 0x3A, 0xBD, 0x1B, 0xA5, 0x19, 0x01, 0x25, 0x73, + // ]), + macKey: deriveMACKey(alwaysEncryptedCEK), + + // ivKey: e45dfdea81075d68ef80e4eee4cee69f55b5dd96c8d1d9afbcc895f0c17e2bcb + // ivKey: Buffer.from([ + // 0xE4, 0x5D, 0xFD, 0xEA, 0x81, 0x07, 0x5D, 0x68, + // 0xEF, 0x80, 0xE4, 0xEE, 0xE4, 0xCE, 0xE6, 0x9F, + // 0x55, 0xB5, 0xDD, 0x96, 0xC8, 0xD1, 0xD9, 0xAF, + // 0xBC, 0xC8, 0x95, 0xF0, 0xC1, 0x7E, 0x2B, 0xCB, + // ]), + ivKey: deriveIVKey(alwaysEncryptedCEK), +}; + +const alwaysEncryptedOptions = { + ...options, + serverSupportsColumnEncryption: true, + trustedServerNameAE: 'localhost', + encryptionKeyStoreProviders: { + 'TEST_KEYSTORE': { + decryptColumnEncryptionKey: () => Promise.resolve(alwaysEncryptedCEK), + }, + }, +}; +const cryptoMetadata = { + cekEntry: { + ordinal: 0x01, + databaseId: 0x00, + cekId: 0x00, + cekVersion: 0x00, + cekMdVersion: 0x00, + columnEncryptionKeyValues: [{ + encryptedKey: Buffer.from([ 0x00 ]), + dbId: 0x05, + keyId: 0x31, + keyVersion: 0x01, + mdVersion: Buffer.from([ + 0xF1, 0x08, 0x60, 0x01, 0xE8, 0xAA, 0x00, 0x00, + ]), + keyPath: 'test', + keyStoreName: 'TEST_KEYSTORE', + algorithmName: 'RSA_OAEP', + }], + }, + cipherAlgorithmId: 0x02, + encryptionType: 0x01, + normalizationRuleVersion: Buffer.from([ 0x01 ]), +}; + +module.exports = { + algorithmName, + deriveEncryptionKey, + deriveIVKey, + deriveMACKey, + + generateEncryptedVarBinary, + + alwaysEncryptedCEK, + alwaysEncryptedIV, + alwaysEncryptedAlgorithmName, + alwaysEncryptedConstants, + alwaysEncryptedOptions, + cryptoMetadata, +}; diff --git a/test/unit/login7-payload-test.js b/test/unit/login7-payload-test.js index a1e866131..b9b464df0 100644 --- a/test/unit/login7-payload-test.js +++ b/test/unit/login7-payload-test.js @@ -233,5 +233,54 @@ describe('Login7Payload', function() { assert.lengthOf(data, expectedLength); }); }); + + describe('for a login payload with column encryption data', function() { + it('generates the expected data', function() { + const payload = new Login7Payload({ + tdsVersion: 0x72090002, + packetSize: 1024, + clientProgVer: 0, + clientPid: 12345, + connectionId: 0, + clientTimeZone: 120, + clientLcid: 0x00000409, + }); + + payload.hostname = 'example.com'; + payload.appName = 'app'; + payload.serverName = 'server'; + payload.language = 'lang'; + payload.database = 'db'; + payload.libraryName = 'Tedious'; + payload.attachDbFile = 'c:\\mydbfile.mdf'; + payload.changePassword = 'new_pw'; + payload.columnEncryption = true; + + const data = payload.toBuffer(); + + const expectedLength = + 4 + // Length + 32 + // Fixed data + // Variable + 2 + 2 + (2 * payload.hostname.length) + + 2 + 2 + 2 * 0 + // Username + 2 + 2 + 2 * 0 + // Password + 2 + 2 + (2 * payload.appName.length) + + 2 + 2 + (2 * payload.serverName.length) + + 2 + 2 + 4 + + 2 + 2 + (2 * payload.libraryName.length) + + 2 + 2 + (2 * payload.language.length) + + 2 + 2 + (2 * payload.database.length) + + 6 + // ClientID + 2 + 2 + (2 * payload.attachDbFile.length) + + 2 + 2 + (2 * payload.changePassword.length) + + 4 + // cbSSPILong + 4 + // Extension offset + 1 + 4 + 1 + // column encryption + 1;// Feature ext + + assert.lengthOf(data, expectedLength); + }); + }); }); }); diff --git a/test/unit/token/colmetadata-token-parser-test.js b/test/unit/token/colmetadata-token-parser-test.js index f0ab99c27..c42bc2442 100644 --- a/test/unit/token/colmetadata-token-parser-test.js +++ b/test/unit/token/colmetadata-token-parser-test.js @@ -1,6 +1,7 @@ const dataTypeByName = require('../../../src/data-type').typeByName; const WritableTrackingBuffer = require('../../../src/tracking-buffer/writable-tracking-buffer'); const StreamParser = require('../../../src/token/stream-parser'); +const { alwaysEncryptedOptions } = require('../always-encrypted/crypto-util'); const assert = require('chai').assert; describe('Colmetadata Token Parser', () => { @@ -66,6 +67,7 @@ describe('Colmetadata Token Parser', () => { assert.isFalse(result.done); const token = result.value; + assert.isNotNull(token); assert.isOk(!token.error); assert.strictEqual(token.columns.length, 1); assert.strictEqual(token.columns[0].userType, 2); @@ -114,4 +116,310 @@ describe('Colmetadata Token Parser', () => { assert.strictEqual(token.columns[0].colName, 'name'); assert.strictEqual(token.columns[0].dataLength, length); }); + + it('should parse crypto-metadata', async () => { + const alwaysEncryptedOptionsLocal = { ...alwaysEncryptedOptions }; + const alwaysEncryptedCEK = Buffer.from([ + // decrypted column key must be 32 bytes long for AES256 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + + alwaysEncryptedOptionsLocal.encryptionKeyStoreProviders = { + 'MSSQL_JAVA_KEYSTORE': { + decryptColumnEncryptionKey: () => Promise.resolve(alwaysEncryptedCEK), + }, + }; + + const buffer = Buffer.from([ + // ----- + // framing seq ... + // 0x04, 0x01, 0x02, 0xE7, 0x00, 0x34, 0x01, 0x00, + + 0x81, // token type (COLMETADATA) + 0x01, 0x00, // number of columns (1) + + // ----- + // CEK table (server must support column encryption) + 0x01, 0x00, // cek table size (1) + + // CEK entry 1 + 0x05, 0x00, 0x00, 0x00, // database id (5) + 0x31, 0x00, 0x00, 0x00, // key id (49) + 0x01, 0x00, 0x00, 0x00, // key version (1) + 0xF1, 0x08, 0x60, 0x01, // digest version + 0xE8, 0xAA, 0x00, 0x00, // (cont.d) + 0x01, // cek value count (1) + + // CEK entry 1, value 1 + 0x0D, 0x02, // encrypted cek len (525) + // encrypted cek data ... (525 bytes) + 0x01, 0x08, 0x00, 0x00, + 0x01, 0x74, 0x00, 0x65, 0x00, 0x73, 0x00, 0x74, + 0x00, 0xA7, 0x31, 0x79, 0xA5, 0x48, 0xEC, 0xA9, + 0x2E, 0x83, 0x66, 0x3D, 0x71, 0x7A, 0x01, 0x43, + 0x3A, 0x26, 0xDE, 0x1D, 0x25, 0xFB, 0x39, 0x26, + 0xE3, 0x58, 0x5C, 0x35, 0x38, 0x4D, 0x99, 0xE1, + 0x23, 0x48, 0xEA, 0x87, 0x61, 0xED, 0x15, 0x6E, + 0x64, 0xF2, 0x78, 0x87, 0xFC, 0x90, 0x5A, 0xFE, + 0x2C, 0x5A, 0x97, 0xDF, 0x79, 0x7C, 0x4F, 0x42, + 0x13, 0xCC, 0xA1, 0xAA, 0x81, 0x76, 0xD1, 0x21, + 0x99, 0xD4, 0xB6, 0x5D, 0x0F, 0x1E, 0xAD, 0x6C, + 0x0B, 0x5E, 0x90, 0xA2, 0xA6, 0xFC, 0x18, 0x07, + 0x77, 0x1D, 0x3A, 0x7D, 0x8A, 0x8B, 0x67, 0xAA, + 0xDD, 0xF6, 0x65, 0xAF, 0xA1, 0x20, 0x14, 0xCC, + 0xB9, 0x95, 0x06, 0xAB, 0x8E, 0xDC, 0x76, 0x56, + 0xAB, 0x55, 0xC5, 0x97, 0xC8, 0x07, 0x7E, 0xA3, + 0xB8, 0x96, 0x45, 0x84, 0x1A, 0xDE, 0x51, 0x7F, + 0xB0, 0x70, 0x12, 0x00, 0x56, 0x0F, 0x97, 0xCB, + 0xB6, 0xFC, 0x9B, 0x14, 0xAF, 0x15, 0x9A, 0x7A, + 0x11, 0x09, 0xA7, 0x7D, 0xDE, 0x46, 0xA4, 0x31, + 0xBD, 0xCF, 0x43, 0x9A, 0xC1, 0x34, 0xC7, 0x2A, + 0xF6, 0x9E, 0xF5, 0xC9, 0xE2, 0xF0, 0x39, 0xCB, + 0xA4, 0xCF, 0x64, 0x4E, 0xB5, 0x49, 0xC1, 0x23, + 0x00, 0xD7, 0x2C, 0x75, 0x1E, 0x6A, 0x00, 0x8E, + 0x2C, 0x0A, 0x57, 0x53, 0x4F, 0xBB, 0x51, 0xE7, + 0xA2, 0x6B, 0x7F, 0xAF, 0xCA, 0x6C, 0x28, 0x5F, + 0x94, 0xD8, 0x0F, 0x44, 0x57, 0xE3, 0xA7, 0x44, + 0x37, 0x3A, 0x7A, 0xB3, 0xB7, 0xBA, 0x33, 0xBF, + 0x90, 0xF9, 0x8C, 0xD2, 0x4E, 0x5B, 0x7F, 0x84, + 0x2E, 0x29, 0x3F, 0xE7, 0x12, 0xBE, 0x01, 0xB7, + 0xC8, 0x04, 0x2D, 0xAE, 0x03, 0x20, 0xFF, 0x2C, + 0xD2, 0x51, 0x4B, 0xD7, 0x72, 0x93, 0x6E, 0x3B, + 0x2D, 0x27, 0xAB, 0xBE, 0x75, 0x36, 0x7F, 0x75, + 0x8B, 0xDD, 0xB6, 0xF6, 0xFB, 0x13, 0x61, 0x90, + 0xDA, 0x0E, 0x35, 0x23, 0x22, 0xBF, 0x49, 0x82, + 0xA7, 0xAB, 0xCB, 0x63, 0xA3, 0x3C, 0xE6, 0xDA, + 0x8B, 0x73, 0x35, 0xD3, 0x3F, 0xB4, 0x6E, 0xA0, + 0x63, 0x32, 0x79, 0x9F, 0x01, 0x85, 0xD7, 0x47, + 0x43, 0xE5, 0xBC, 0x6C, 0x31, 0x8D, 0x7E, 0x7E, + 0x17, 0xC5, 0x61, 0x3F, 0x37, 0x31, 0x41, 0x3D, + 0x41, 0x23, 0x60, 0xA8, 0xE4, 0x1E, 0xBF, 0x2E, + 0xBC, 0x14, 0x12, 0x0D, 0x11, 0xD3, 0xEB, 0x74, + 0x39, 0x95, 0xB3, 0x09, 0x3B, 0x40, 0x6C, 0xD2, + 0x19, 0x53, 0x93, 0xEC, 0x7A, 0x75, 0x94, 0x46, + 0x29, 0x8F, 0x32, 0xF4, 0xFC, 0xE4, 0xB9, 0xF7, + 0xF6, 0x5F, 0x22, 0x5A, 0xD8, 0x33, 0xCA, 0x58, + 0x9D, 0x52, 0xF8, 0x22, 0xFA, 0xB3, 0x18, 0xEE, + 0xDE, 0x83, 0x82, 0xAA, 0x90, 0xC9, 0x29, 0xF9, + 0x4F, 0x17, 0xA1, 0xD6, 0x87, 0x55, 0x3D, 0x1E, + 0xDB, 0xF3, 0xD6, 0xCF, 0x87, 0xB0, 0x5E, 0x57, + 0x35, 0x0C, 0x07, 0x64, 0x96, 0xFD, 0x2E, 0x30, + 0xEF, 0x32, 0x85, 0x6F, 0xE4, 0xF8, 0x5F, 0xA1, + 0x5E, 0x50, 0x04, 0x36, 0x0D, 0xAA, 0x80, 0x18, + 0xC9, 0x0C, 0xCB, 0x21, 0x08, 0xA3, 0x17, 0xCB, + 0xD3, 0x5C, 0xFF, 0x18, 0x4A, 0x5B, 0x9A, 0xB6, + 0x5F, 0x46, 0x43, 0x76, 0x30, 0x0C, 0x6B, 0x94, + 0xF0, 0x2F, 0xFE, 0x5C, 0xF1, 0x57, 0x9E, 0x79, + 0x3B, 0x1A, 0xD0, 0x6C, 0xBE, 0xF4, 0x9D, 0xF5, + 0x40, 0x1C, 0xF4, 0xB3, 0x53, 0x73, 0xCA, 0xEE, + 0xD2, 0xF6, 0xC7, 0xB6, 0xEA, 0x38, 0x7F, 0xDB, + 0x46, 0x06, 0x63, 0x8C, 0xFC, 0x86, 0x1D, 0xF4, + 0xD1, 0xA3, 0xE4, 0x16, 0x9A, 0xEF, 0x31, 0x56, + 0x4D, 0x19, 0xD7, 0xAE, 0x5B, 0x4D, 0x4E, 0xB5, + 0xDD, 0x47, 0x1A, 0x1B, 0x6D, 0x1E, 0x8D, 0x2B, + 0x63, 0x2B, 0xB7, 0x11, 0x35, 0xE5, 0x3C, 0x52, + 0x5A, 0x5A, 0xA6, 0x27, 0x89, 0x09, 0xA6, 0xB6, + 0xF4, + + 0x13, // keystore name len (19) + // keystore name ... (19 x2 bytes) + + 0x4D, 0x00, // 'M' + 0x53, 0x00, // 'S' + 0x53, 0x00, // 'S' + 0x51, 0x00, // 'Q' + 0x4C, 0x00, // 'L' + 0x5F, 0x00, // '_' + 0x4A, 0x00, // 'J' + 0x41, 0x00, // 'A' + 0x56, 0x00, // 'V' + 0x41, 0x00, // 'A' + 0x5F, 0x00, // '_' + 0x4B, 0x00, // 'K' + 0x45, 0x00, // 'E' + 0x59, 0x00, // 'Y' + 0x53, 0x00, // 'S' + 0x54, 0x00, // 'T' + 0x4F, 0x00, // 'O' + 0x52, 0x00, // 'R' + 0x45, 0x00, // 'E' + + 0x04, // key path length (4) + + // key path ... (4 x2 bytes) + 0x00, 0x74, // 't' + 0x00, 0x65, // 'e' + 0x00, 0x73, // 's' + 0x00, 0x74, // 't' + + 0x00, 0x08, // algo name length (8) + + // algo name ... (8 x2 bytes) + 0x52, 0x00, // 'R' + 0x53, 0x00, // 'S' + 0x41, 0x00, // 'A' + 0x5F, 0x00, // '_' + 0x4F, 0x00, // 'O' + 0x41, 0x00, // 'A' + 0x45, 0x00, // 'E' + 0x50, 0x00, // 'P' + + // ----- + // Column 1 + 0x00, 0x00, 0x00, 0x00, // user type (0) + 0x0B, 0x08, // flags (2059) + 0xA5, // data type (BIGVARBIN) + + // varbinary ... + 0x41, 0x02, // varbinary len (577) + // no precision + // no scale + + // no collation + // no schema + // no user-defined type + + // no column-level table data + + // Column 1, crypto metadata + 0x00, 0x00, // crypto meta ord (0) + 0x00, 0x00, 0x00, 0x00, // crypto user type (0) + 0xA7, // crypto data type (BIGVARCHR) + // no flags + + // varchar ... + 0x00, 0x02, // varchar len (512) + // no precision + // no scale + + // collation ... + 0x09, 0x04, 0x00, // lcid (1033) + 0x02, // flags (0) + // version (2) + 0x00, // sortid (0) + // => codepage CP1252 + + // no schema + // no user-defined type + 0x02, // crypto algo id (2) + // no crypto algo name + + 0x01, // crypto enc type (1) + 0x01, // norm rule vers (1) + + // Column 1, column info + 0x04, // col name len (4) + 0x4E, 0x00, // 'N' + 0x61, 0x00, // 'a' + 0x6D, 0x00, // 'm' + 0x65, 0x00, // 'e' + + // ----- + // row data ... + // 0xD1, 0x41, + // 0x00, 0x01, 0x89, 0xE1, 0x38, 0xB4, 0xA2, 0x5B, + // 0x62, 0xE2, 0x5E, 0x49, 0x95, 0xE9, 0x19, 0xDC, + // 0x46, 0x63, 0xB1, 0x45, 0x86, 0x07, 0x5C, 0x53, + // 0x9F, 0xB1, 0x54, 0x6E, 0x64, 0x98, 0xBA, 0xD9, + // 0xF6, 0x48, 0x46, 0x95, 0xB9, 0xD4, 0x3D, 0xF8, + // 0xE6, 0x55, 0xEA, 0x48, 0xFF, 0xE4, 0xDD, 0x9F, + // 0xA1, 0x17, 0x70, 0xD5, 0xA1, 0xF8, 0x44, 0x68, + // 0x8D, 0x62, 0xA3, 0x47, 0x76, 0x0A, 0x4E, 0xCF, + // 0x19, 0xBD, 0xFD, 0x10, 0x00, 0xC1, 0x00, 0x01, + // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + // console.log(buffer); + + const parser = StreamParser.parseTokens([buffer], {}, alwaysEncryptedOptionsLocal); + const result = await parser.next(); + + assert.isFalse(result.done); + const token = result.value; + assert.isOk(!token.error); + + assert.isNotNull(token); + assert.isOk(!token.error); + assert.strictEqual(token.columns.length, 0x01); + + const column = token.columns[0]; + assert.isNotNull(column); + assert.strictEqual(column.userType, 0x00); + assert.strictEqual(column.flags, 0x080B); + assert.strictEqual(column.colName, 'Name'); + assert.strictEqual(column.dataLength, 0x0241); + + assert.isNotNull(column.type); + const columnType = column.type; + assert.strictEqual(columnType.id, 0xA5); + assert.strictEqual(columnType.type, 'BIGVARBIN'); + assert.strictEqual(columnType.name, 'VarBinary'); + + assert.isNotNull(column.collation); + const columnCollation = column.collation; + assert.strictEqual(columnCollation.lcid, 0x0409); + assert.strictEqual(columnCollation.flags, 0x20); + assert.strictEqual(columnCollation.version, 0x00); + assert.strictEqual(columnCollation.sortId, 0x00); + assert.strictEqual(columnCollation.codepage, 'CP1252'); + + assert.isNotNull(column.cryptoMetadata); + const columnCryptoMetadata = column.cryptoMetadata; + assert.strictEqual(columnCryptoMetadata.ordinal, 0x00); + assert.strictEqual(columnCryptoMetadata.cipherAlgorithmId, 0x02); + assert.strictEqual(columnCryptoMetadata.encryptionType, 0x01); + assert.deepEqual( + columnCryptoMetadata.normalizationRuleVersion, + Buffer.from([0x01]), + ); + + assert.isNotNull(columnCryptoMetadata.cekEntry); + const columnCryptoCekTableEntry = columnCryptoMetadata.cekEntry; + assert.strictEqual(columnCryptoCekTableEntry.ordinal, 0x01); + assert.strictEqual(columnCryptoCekTableEntry.databaseId, 0x05); + assert.strictEqual(columnCryptoCekTableEntry.cekId, 0x31); + assert.strictEqual(columnCryptoCekTableEntry.cekVersion, 0x01); + assert.deepEqual( + columnCryptoCekTableEntry.cekMdVersion, + Buffer.from([0xF1, 0x08, 0x60, 0x01, 0xE8, 0xAA, 0x00, 0x00]), + ); + + assert.strictEqual(columnCryptoCekTableEntry.columnEncryptionKeyValues.length, 0x01); + const columnEncryptionKeyValue = columnCryptoCekTableEntry.columnEncryptionKeyValues[0]; + assert.isNotNull(columnEncryptionKeyValue); + assert.strictEqual(columnEncryptionKeyValue.dbId, 0x05); + assert.strictEqual(columnEncryptionKeyValue.keyId, 0x31); + assert.strictEqual(columnEncryptionKeyValue.keyVersion, 0x01); + assert.deepEqual( + columnEncryptionKeyValue.mdVersion, + Buffer.from([0xF1, 0x08, 0x60, 0x01, 0xE8, 0xAA, 0x00, 0x00]), + ); + assert.strictEqual(columnEncryptionKeyValue.keyPath, 'test'); + assert.strictEqual(columnEncryptionKeyValue.keyStoreName, 'MSSQL_JAVA_KEYSTORE'); + assert.strictEqual(columnEncryptionKeyValue.algorithmName, 'RSA_OAEP'); + + assert.isNotNull(columnCryptoMetadata.baseTypeInfo); + const columnCryptoBaseTypeInfo = columnCryptoMetadata.baseTypeInfo; + assert.strictEqual(columnCryptoBaseTypeInfo.userType, 0x00); + assert.strictEqual(columnCryptoBaseTypeInfo.flags, 0x080B); + assert.strictEqual(columnCryptoBaseTypeInfo.dataLength, 0x0200); + + assert.isNotNull(columnCryptoBaseTypeInfo.type); + const columnCryptoBaseTypeType = columnCryptoBaseTypeInfo.type; + assert.strictEqual(columnCryptoBaseTypeType.id, 0xA7); + assert.strictEqual(columnCryptoBaseTypeType.type, 'BIGVARCHR'); + assert.strictEqual(columnCryptoBaseTypeType.name, 'VarChar'); + + assert.isNotNull(columnCryptoBaseTypeInfo.collation); + const columnCryptoBaseTypeCollation = columnCryptoBaseTypeInfo.collation; + assert.strictEqual(columnCryptoBaseTypeCollation.lcid, 0x0409); + assert.strictEqual(columnCryptoBaseTypeCollation.flags, 0x20); + assert.strictEqual(columnCryptoBaseTypeCollation.version, 0x00); + assert.strictEqual(columnCryptoBaseTypeCollation.sortId, 0x00); + assert.strictEqual(columnCryptoBaseTypeCollation.codepage, 'CP1252'); + + assert.isTrue((await parser.next()).done); + }); }); diff --git a/test/unit/token/feature-ext-parser-test.js b/test/unit/token/feature-ext-parser-test.js index d76b1d637..56e8399b8 100644 --- a/test/unit/token/feature-ext-parser-test.js +++ b/test/unit/token/feature-ext-parser-test.js @@ -51,4 +51,45 @@ describe('Feature Ext Parser', () => { assert.isTrue((await parser.next()).done); }); + + it('should parse column encryption token', async () => { + const buffer = new WritableTrackingBuffer(8); + + buffer.writeUInt8(0xAE); // FEATUREEXTACK token header + buffer.writeUInt8(0x04); // COLUMNENCRYPTION feature id + buffer.writeUInt32LE(0x00_00_00_01); // datalen + buffer.writeUInt8(0x01); // supported + + buffer.writeUInt8(0xFF); // TERMINATOR + + const parser = StreamParser.parseTokens([buffer.data], {}, {}); + const result = await parser.next(); + assert.isFalse(result.done); + + const token = result.value; + assert.strictEqual(token.columnEncryption, true); // feature ext ack for COLUMNENCRYPTION was positive + assert.isUndefined(token.fedAuth); // fed auth not ack'd + + assert.isTrue((await parser.next()).done); + }); + + it('should return error for non support cryptographic protocol version', async () => { + const buffer = new WritableTrackingBuffer(8); + + buffer.writeUInt8(0xAE); // FEATUREEXTACK token header + buffer.writeUInt8(0x04); // COLUMNENCRYPTION feature id + buffer.writeUInt32LE(0x00_00_00_01); // datalen + buffer.writeUInt8(0x02); // supported + + buffer.writeUInt8(0xFF); // TERMINATOR + let error; + try { + const parser = StreamParser.parseTokens([buffer.data], {}, {}); + await parser.next(); + } catch (err) { + error = err; + } + assert.instanceOf(error, Error); + assert.include(error.message, 'Unsupported supported cryptographic protocol version'); + }); });