diff --git a/src/always-encrypted/key-crypto.ts b/src/always-encrypted/key-crypto.ts index 9fab125e3..9588b48e5 100644 --- a/src/always-encrypted/key-crypto.ts +++ b/src/always-encrypted/key-crypto.ts @@ -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'; @@ -16,7 +16,7 @@ export const validateAndGetEncryptionAlgorithmName = (cipherAlgorithmId: number, return algorithmName; }; -export const encryptWithKey = async (plaintext: Buffer, md: CryptoMetadata, options: ConnectionOptions): Promise => { +export const encryptWithKey = async (plaintext: Buffer, md: CryptoMetadata, options: ParserOptions): Promise => { if (!options.trustedServerNameAE) { throw new Error('Server name should not be null in EncryptWithKey'); } @@ -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 => { 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'); @@ -60,7 +60,7 @@ export const decryptWithKey = (cipherText: Buffer, md: CryptoMetadata, options: return plainText; }; -export const decryptSymmetricKey = async (md: CryptoMetadata, options: ConnectionOptions): Promise => { +export const decryptSymmetricKey = async (md: CryptoMetadata, options: ParserOptions): Promise => { if (!md) { throw new Error('md should not be null in DecryptSymmetricKey.'); } diff --git a/src/always-encrypted/symmetric-key-cache.ts b/src/always-encrypted/symmetric-key-cache.ts index 81d874f19..93e68d438 100644 --- a/src/always-encrypted/symmetric-key-cache.ts +++ b/src/always-encrypted/symmetric-key-cache.ts @@ -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(0); -export const getKey = async (keyInfo: EncryptionKeyInfo, options: ConnectionOptions): Promise => { +export const getKey = async (keyInfo: EncryptionKeyInfo, options: ParserOptions): Promise => { if (!options.trustedServerNameAE) { throw new Error('Server name should not be null in getKey'); } diff --git a/src/connection.ts b/src/connection.ts index f41e0b509..a8e6844cf 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -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'; @@ -397,6 +399,11 @@ interface KeyStoreProviderMap { [key: string]: ColumnEncryptionAzureKeyVaultProvider; } +interface KeyStoreProvider { + key: string; + value: ColumnEncryptionAzureKeyVaultProvider; +} + /** * @private */ @@ -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 @@ -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 @@ -871,6 +884,7 @@ interface RoutingData { port: number; } + /** * A [[Connection]] instance represents a single connection to a database server. * @@ -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.'); @@ -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; @@ -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) { @@ -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. * diff --git a/src/request.ts b/src/request.ts index 6b6b4f30c..db87d8101 100644 --- a/src/request.ts +++ b/src/request.ts @@ -37,6 +37,8 @@ export interface ParameterOptions { length?: number; precision?: number; scale?: number; + + forceEncrypt?: boolean; } interface RequestOptions { @@ -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 | null) { - const { output = false, length, precision, scale } = options ?? {}; + const { output = false, length, precision, scale, forceEncrypt = false } = options ?? {}; const parameter: Parameter = { type: type, @@ -408,7 +410,8 @@ class Request extends EventEmitter { output: output, length: length, precision: precision, - scale: scale + scale: scale, + forceEncrypt: forceEncrypt }; this.parameters.push(parameter); diff --git a/src/token/row-token-parser.ts b/src/token/row-token-parser.ts index f394dbf97..3bd061f42 100644 --- a/src/token/row-token-parser.ts +++ b/src/token/row-token-parser.ts @@ -4,10 +4,10 @@ import Parser from './stream-parser'; import { type ColumnMetadata } from './colmetadata-token-parser'; import { RowToken } from './token'; -import * as iconv from 'iconv-lite'; +// import * as iconv from 'iconv-lite'; -import { isPLPStream, readPLPStream, readValue } from '../value-parser'; -import { NotEnoughDataError } from './helpers'; +import { readValue, readDecrypt } from '../value-parser'; +// import { NotEnoughDataError } from './helpers'; interface Column { value: unknown; @@ -16,40 +16,9 @@ interface Column { async function rowParser(parser: Parser): Promise { const columns: Column[] = []; - for (const metadata of parser.colMetadata) { - while (true) { - if (isPLPStream(metadata)) { - const chunks = await readPLPStream(parser); - - if (chunks === null) { - columns.push({ value: chunks, metadata }); - } else if (metadata.type.name === 'NVarChar' || metadata.type.name === 'Xml') { - columns.push({ value: Buffer.concat(chunks).toString('ucs2'), metadata }); - } else if (metadata.type.name === 'VarChar') { - columns.push({ value: iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), metadata }); - } else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') { - columns.push({ value: Buffer.concat(chunks), metadata }); - } - } else { - let result; - try { - result = readValue(parser.buffer, parser.position, metadata, parser.options); - } catch (err) { - if (err instanceof NotEnoughDataError) { - await parser.waitForChunk(); - continue; - } - - throw err; - } - - parser.position = result.offset; - columns.push({ value: result.value, metadata }); - } - - break; - } + const result = await readDecrypt(parser, metadata, parser.options); + columns.push({ value: result.value, metadata }); } if (parser.options.useColumnNames) { diff --git a/src/token/stream-parser.ts b/src/token/stream-parser.ts index 6b9a7f516..b659d97b2 100644 --- a/src/token/stream-parser.ts +++ b/src/token/stream-parser.ts @@ -16,9 +16,10 @@ import returnValueParser from './returnvalue-token-parser'; import rowParser from './row-token-parser'; import nbcRowParser from './nbcrow-token-parser'; import sspiParser from './sspi-token-parser'; +import { decryptSymmetricKey } from '../always-encrypted/key-crypto'; import { NotEnoughDataError } from './helpers'; -export type ParserOptions = Pick; +export type ParserOptions = Pick; class Parser { debug: Debug; @@ -158,6 +159,11 @@ class Parser { async readColMetadataToken(): Promise { const token = await colMetadataParser(this); this.colMetadata = token.columns; + await Promise.all(this.colMetadata.map(async (metadata) => { + if (metadata.cryptoMetadata) { + await decryptSymmetricKey(metadata.cryptoMetadata, this.options); + } + })); return token; } diff --git a/src/value-parser.ts b/src/value-parser.ts index 05f0e4d9a..d573de51c 100644 --- a/src/value-parser.ts +++ b/src/value-parser.ts @@ -6,6 +6,9 @@ import iconv from 'iconv-lite'; import { sprintf } from 'sprintf-js'; import { bufferToLowerCaseGuid, bufferToUpperCaseGuid } from './guid-parser'; import { NotEnoughDataError, Result, readBigInt64LE, readDoubleLE, readFloatLE, readInt16LE, readInt32LE, readUInt16LE, readUInt32LE, readUInt8, readUInt24LE, readUInt40LE, readUNumeric64LE, readUNumeric96LE, readUNumeric128LE } from './token/helpers'; +import { decryptWithKey } from './always-encrypted/key-crypto'; +import { type CryptoMetadata } from './always-encrypted/types'; +import { isUndefined } from 'util'; const NULL = (1 << 16) - 1; const MAX = (1 << 16) - 1; @@ -14,6 +17,7 @@ const MONEY_DIVISOR = 10000; const PLP_NULL = 0xFFFFFFFFFFFFFFFFn; const UNKNOWN_PLP_LEN = 0xFFFFFFFFFFFFFFFEn; const DEFAULT_ENCODING = 'utf8'; +const SUPPORTED_NOM_VER = Buffer.from([0x01]); function readTinyInt(buf: Buffer, offset: number): Result { return readUInt8(buf, offset); @@ -45,7 +49,6 @@ function readFloat(buf: Buffer, offset: number): Result { function readSmallMoney(buf: Buffer, offset: number): Result { let value; ({ offset, value } = readInt32LE(buf, offset)); - return new Result(value / MONEY_DIVISOR, offset); } @@ -68,7 +71,7 @@ function readBit(buf: Buffer, offset: number): Result { function readValue(buf: Buffer, offset: number, metadata: Metadata, options: ParserOptions): Result { const type = metadata.type; - + let dataLength = metadata.dataLength; switch (type.name) { case 'Null': return new Result(null, offset); @@ -90,9 +93,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'IntN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); - + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: return new Result(null, offset); @@ -120,8 +123,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'FloatN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: @@ -145,9 +149,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par return readMoney(buf, offset); case 'MoneyN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); - + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: return new Result(null, offset); @@ -167,8 +171,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'BitN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: @@ -186,8 +191,13 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par case 'Char': { const codepage = metadata.collation!.codepage!; - let dataLength; - ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + if (undefined === dataLength) { + if (options.serverSupportsColumnEncryption && undefined === metadata.cryptoMetadata) { + dataLength = buf.length; + } else { + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + } + } if (dataLength === NULL) { return new Result(null, offset); @@ -198,25 +208,35 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par case 'NVarChar': case 'NChar': { - let dataLength; - ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + if (undefined === dataLength) { + if (options.serverSupportsColumnEncryption && undefined === metadata.cryptoMetadata) { + dataLength = buf.length; + } else { + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + } + } if (dataLength === NULL) { return new Result(null, offset); } - return readNChars(buf, offset, dataLength); } case 'VarBinary': case 'Binary': { - let dataLength; - ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + if (undefined === dataLength) { + if (options.serverSupportsColumnEncryption && undefined === metadata.cryptoMetadata) { + dataLength = buf.length; + } else { + ({ offset, value: dataLength } = readUInt16LE(buf, offset)); + } + } if (dataLength === NULL) { return new Result(null, offset); } + return readBinary(buf, offset, dataLength); } @@ -289,8 +309,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'DateTimeN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: @@ -307,8 +328,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'Time': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } if (dataLength === 0) { return new Result(null, offset); @@ -318,8 +340,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'Date': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } if (dataLength === 0) { return new Result(null, offset); @@ -329,8 +352,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'DateTime2': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } if (dataLength === 0) { return new Result(null, offset); @@ -340,8 +364,9 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par } case 'DateTimeOffset': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } if (dataLength === 0) { return new Result(null, offset); @@ -352,19 +377,25 @@ function readValue(buf: Buffer, offset: number, metadata: Metadata, options: Par case 'NumericN': case 'DecimalN': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } if (dataLength === 0) { return new Result(null, offset); } + if (options.serverSupportsColumnEncryption && undefined === metadata.cryptoMetadata) { + return readEncryptNumeric(buf, offset, dataLength, metadata.precision!, metadata.scale!); + } + return readNumeric(buf, offset, dataLength, metadata.precision!, metadata.scale!); } case 'UniqueIdentifier': { - let dataLength; - ({ offset, value: dataLength } = readUInt8(buf, offset)); + if (undefined === dataLength) { + ({ offset, value: dataLength } = readUInt8(buf, offset)); + } switch (dataLength) { case 0: @@ -442,6 +473,17 @@ function readNumeric(buf: Buffer, offset: number, dataLength: number, _precision return new Result((value * sign) / Math.pow(10, scale), offset); } +function readEncryptNumeric(buf: Buffer, offset: number, dataLength: number, _precision: number, scale: number): Result { + let sign; + ({ offset, value: sign } = readUInt8(buf, offset)); + + sign = sign === 1 ? 1 : -1; + + const value = buf.slice(offset, offset + dataLength).reduceRight((acc, byte) => acc * (1 << 8) + byte);; + + return new Result((value * sign) / Math.pow(10, scale), offset + dataLength); +} + function readVariant(buf: Buffer, offset: number, options: ParserOptions, dataLength: number): Result { let baseType; ({ value: baseType, offset } = readUInt8(buf, offset)); @@ -541,7 +583,7 @@ function readVariant(buf: Buffer, offset: number, options: ParserOptions, dataLe let collation; ({ value: collation, offset } = readCollation(buf, offset)); - return readChars(buf, offset, dataLength, collation.codepage!); + return readChars(buf, offset, buf.length, collation.codepage!); } case 'NVarChar': @@ -762,9 +804,80 @@ function readDateTimeOffset(buf: Buffer, offset: number, dataLength: number, sca }); return new Result(date, offset); } +async function readData(parser: Parser, buf: Buffer, offset: number, metadata: Metadata, options: ParserOptions): Promise> { + let result; + while (true) { + if (isPLPStream(metadata)) { + let chunks; + if (options.serverSupportsColumnEncryption && undefined === metadata.cryptoMetadata) { + chunks = [buf]; + if (buf.length >= 8 && buf.readBigUInt64LE(offset) === PLP_NULL) { + chunks = null; + } + } else { + chunks = await readPLPStream(parser); + } + + if (chunks === null) { + result = new Result(chunks, parser.position); + } else if (metadata.type.name === 'NVarChar' || metadata.type.name === 'Xml') { + result = new Result(Buffer.concat(chunks).toString('ucs2'), parser.position); + } else if (metadata.type.name === 'VarChar') { + result = new Result(iconv.decode(Buffer.concat(chunks), metadata.collation?.codepage ?? 'utf8'), parser.position); + } else if (metadata.type.name === 'VarBinary' || metadata.type.name === 'UDT') { + result = new Result(Buffer.concat(chunks), parser.position); + } + } else { + try { + result = readValue(buf, offset, metadata, parser.options); + } catch (err) { + if (err instanceof NotEnoughDataError) { + await parser.waitForChunk(); + continue; + } + + throw err; + } + + offset = result.offset; + } + + break; + } + return result!; +} + +async function readDecrypt(parser: Parser, metadata: Metadata, options: ParserOptions): Promise> { + const result = await readData(parser, parser.buffer, parser.position, metadata, options); + parser.position = result.offset; + + if (metadata.cryptoMetadata) { + const cryptoMetadata: CryptoMetadata = metadata.cryptoMetadata!; + const normalizationRuleVersion = cryptoMetadata.normalizationRuleVersion; + if (!normalizationRuleVersion.equals(SUPPORTED_NOM_VER)) { + throw new Error(`Normalization version "${normalizationRuleVersion}" received from SQL Server is either invalid or corrupted. Valid normalization versions are: ${SUPPORTED_NOM_VER}.`); + } + const decryptedValue = await decryptWithKey(result?.value as Buffer, cryptoMetadata, options); + const baseType = cryptoMetadata.baseTypeInfo; + const colDataType = baseType?.type.name; + switch (colDataType) { + case 'Text': + case 'NText': + case 'Image': + case 'Xml': + case 'UDT': + case 'Variant': + throw new Error(`Unsupported encrypted type "${colDataType}"`); + } + const decryptedResult = await readData(parser, decryptedValue, 0, baseType!, options); + result.value = decryptedResult.value; + } + return result!; +} +module.exports.readDecrypt = readDecrypt; module.exports.readValue = readValue; module.exports.isPLPStream = isPLPStream; module.exports.readPLPStream = readPLPStream; -export { readValue, isPLPStream, readPLPStream }; +export { readValue, isPLPStream, readPLPStream, readDecrypt }; diff --git a/test/integration/always-encrypt/config-tests.js b/test/integration/always-encrypt/config-tests.js new file mode 100644 index 000000000..546167a98 --- /dev/null +++ b/test/integration/always-encrypt/config-tests.js @@ -0,0 +1,154 @@ +const Connection = require('../../../src/connection'); +const Request = require('../../../src/request'); +const TYPES = require('../../../src/data-type').typeByName; +const RequestError = require('../../../src/errors').RequestError; + +const fs = require('fs'); +const { assert } = require('chai'); + +const config = JSON.parse( + fs.readFileSync(require('os').homedir() + '/.tedious/test-connection.json', 'utf8') +).config; + +config.options.debug = { + packet: true, + data: true, + payload: true, + token: true, + log: true +}; +// config.options.columnEncryptionSetting = true; +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, +]); +config.options.encryptionKeyStoreProviders = [{ + key: 'TEST_KEYSTORE', + value: { + decryptColumnEncryptionKey: () => Promise.resolve(alwaysEncryptedCEK), + }, +}]; +config.options.tdsVersion = process.env.TEDIOUS_TDS_VERSION; + +describe('always encrypted', function() { + let connection; + + before(function() { + if (config.options.tdsVersion < '7_4') { + this.skip(); + } + }); + + const createKeys = (cb) => { + const request = new Request(`CREATE COLUMN MASTER KEY [CMK1] WITH ( + KEY_STORE_PROVIDER_NAME = 'TEST_KEYSTORE', + KEY_PATH = 'some-arbitrary-keypath' + );`, (err) => { + if (err) { + return cb(err); + } + const request = new Request(`CREATE COLUMN ENCRYPTION KEY [CEK1] WITH VALUES ( + COLUMN_MASTER_KEY = [CMK1], + ALGORITHM = 'RSA_OAEP', + ENCRYPTED_VALUE = 0xDEADBEEF + );`, (err) => { + if (err) { + return cb(err); + } + return cb(); + }); + connection.execSql(request); + }); + connection.execSql(request); + }; + + const dropKeys = (cb) => { + const request = new Request('IF OBJECT_ID(\'dbo.test_always_encrypted\', \'U\') IS NOT NULL DROP TABLE dbo.test_always_encrypted;', (err) => { + if (err) { + return cb(err); + } + + const request = new Request('IF (SELECT COUNT(*) FROM sys.column_encryption_keys WHERE name=\'CEK1\') > 0 DROP COLUMN ENCRYPTION KEY [CEK1];', (err) => { + if (err) { + return cb(err); + } + + const request = new Request('IF (SELECT COUNT(*) FROM sys.column_master_keys WHERE name=\'CMK1\') > 0 DROP COLUMN MASTER KEY [CMK1];', (err) => { + if (err) { + return cb(err); + } + + cb(); + }); + connection.execSql(request); + }); + connection.execSql(request); + }); + connection.execSql(request); + }; + + beforeEach(function(done) { + connection = new Connection(config); + // connection.on('debug', (msg) => console.log(msg)); + connection.connect((err) => { + if (err) { + return done(err); + } + + dropKeys((err) => { + if (err) { + return done(err); + } + createKeys(done); + }); + }); + }); + + afterEach(function(done) { + if (!connection.closed) { + dropKeys(() => { + connection.on('end', done); + connection.close(); + }); + } else { + done(); + } + }); + + it('should correctly insert/select the encrypted data', function(done) { + const request = new Request(`CREATE TABLE test_always_encrypted ( + [plaintext] nvarchar(50), + [nvarchar_determ_test] nvarchar(50) COLLATE Latin1_General_BIN2 + ENCRYPTED WITH ( + ENCRYPTION_TYPE = DETERMINISTIC, + ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', + COLUMN_ENCRYPTION_KEY = [CEK1] + ) + );`, (err) => { + if (err) { + return done(err); + } + const p1 = 'nvarchar_determ_test_val123'; + const p2 = 'nvarchar_rand_test_val123'; + + const request = new Request('INSERT INTO test_always_encrypted ([plaintext], [nvarchar_determ_test]) VALUES (@p1, @p2)', (err) => { + if (err) { + assert.instanceOf(err, RequestError); + assert.equal(err.message, 'Attempting to insert non-encrypted data into an encrypted table. Ensure config.options.columnEncryptionSetting is set to true'); + return done(); + } + + connection.execSql(request); + }); + + request.addParameter('p1', TYPES.NVarChar, p1); + request.addParameter('p2', TYPES.NVarChar, p2); + + connection.execSql(request); + }); + connection.execSql(request); + }); +}); diff --git a/test/unit/token/row-token-parser-test.js b/test/unit/token/row-token-parser-test.js index 6d9402c57..adde8d42e 100644 --- a/test/unit/token/row-token-parser-test.js +++ b/test/unit/token/row-token-parser-test.js @@ -10,6 +10,15 @@ const NumericN = require('../../../src/data-types/numericn'); const Parser = require('../../../src/token/stream-parser'); const dataTypeByName = require('../../../src/data-type').typeByName; const WritableTrackingBuffer = require('../../../src/tracking-buffer/writable-tracking-buffer'); + +const { + alwaysEncryptedOptions, + generateEncryptedVarBinary, + alwaysEncryptedIV, + alwaysEncryptedCEK, + cryptoMetadata, +} = require('../always-encrypted/crypto-util'); + const options = { useUTC: false, tdsVersion: '7_2' @@ -67,6 +76,42 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted int', async () => { + const baseTypeInfo = { type: dataTypeByName.Int }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + 0x03, 0x00, 0x00, 0x00, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, 0x03); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + + assert.isTrue((await parser.next()).done); + }); + it('should write bigint', async () => { const colMetadata = [ { type: dataTypeByName.BigInt }, @@ -90,6 +135,60 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted bigint', async () => { + const baseTypeInfo = { + type: dataTypeByName.BigInt, + }; + const colMetaDataEntry = { + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }; + const colMetadata = [colMetaDataEntry, colMetaDataEntry]; + + const value1 = Buffer.from([ + // 1 + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]); + const value2 = Buffer.from([ + // 9223372036854775807 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value1, + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value2, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, 2); + assert.strictEqual(token.columns[0].value, '1'); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.strictEqual(token.columns[1].value, '9223372036854775807'); + assert.strictEqual(token.columns[1].metadata, colMetadata[1]); + assert.isTrue((await parser.next()).done); + }); + it('should write real', async () => { const colMetadata = [{ type: dataTypeByName.Real }]; const value = 9.5; @@ -110,6 +209,43 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted real', async () => { + const baseTypeInfo = { + type: dataTypeByName.Real, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + // 9.5 + 0x00, 0x00, 0x18, 0x41, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, 9.5); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write float', async () => { const colMetadata = [{ type: dataTypeByName.Float }]; const value = 9.5; @@ -131,6 +267,44 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted float', async () => { + const baseTypeInfo = { + type: dataTypeByName.Float, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + // 9.5 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x40, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xd1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, 9.5); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write Money', async () => { const colMetadata = [ { type: SmallMoney }, @@ -173,6 +347,92 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted money variants', async () => { + const baseTypeInfo = [ + { type: SmallMoney }, + { type: Money }, + { type: MoneyN, dataLength: 0x00 }, + { type: MoneyN, dataLength: 0x04 }, + { type: MoneyN, dataLength: 0x08 }, + { type: MoneyN, dataLength: 0x08 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const value = 123.456; + const valueLarge = 123456789012345.11; + const expectedValues = [ + value, + value, + null, + value, + value, + valueLarge, + ]; + + const buffer = new WritableTrackingBuffer(0); + buffer.writeUInt8(0xD1); + // despite having a dataLength, all decrypted money types are 8 bytes long + // (they also do not contain the leading dataLength byte) + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x80, 0xd6, 0x12, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x80, 0xD6, 0x12, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x80, 0xd6, 0x12, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x80, 0xD6, 0x12, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0xF4, 0x10, 0x22, 0x11, 0xDC, 0x6A, 0xE9, 0x7D]), + ), + ); + // console.log(buffer.data) + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + }); + it('should write varchar without code page', async () => { const colMetadata = [ { @@ -229,6 +489,115 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varchar without code page', async () => { + const baseTypeInfo = { + userType: 0x00, + flags: 0x080B, + type: dataTypeByName.VarChar, + collation: { codepage: undefined }, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = 'hello world'; + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + // encrypted blob must be formatted correctly + Buffer.from([ + // algorithm version (1 byte) + 0x01, + + // authentication tag (32 bytes) + // calculated with macKey (above), iv, and cipher-text (below) + // e.g. to calculate the expected HMAC + // ( echo -n '01' ; \ + // echo -n '11111111111111111111111111111111' ; \ + // echo -n 'd7cb90a5663df56da49220c09c08f3b9' ; \ + // echo -n '01' ) | \ + // xxd -r -ps | \ + // openssl dgst \ + // -sha256 -mac HMAC \ + // -macopt hexkey:'9d1f2295e509519ed0f1bff77659713280a3651fa2d7a7023abd1ba519012573' + // # af43a7120629065cfbdd00f25462a4f3f2b63091ce312a0bff5c2ca064e555db + 0xAF, 0x43, 0xA7, 0x12, 0x06, 0x29, 0x06, 0x5C, + 0xFB, 0xDD, 0x00, 0xF2, 0x54, 0x62, 0xA4, 0xF3, + 0xF2, 0xB6, 0x30, 0x91, 0xCE, 0x31, 0x2A, 0x0B, + 0xFF, 0x5C, 0x2C, 0xA0, 0x64, 0xE5, 0x55, 0xDB, + + // iv (16 bytes) + // arbitrary, but must be the one used in the authentication tag above + ...alwaysEncryptedIV, + + // cipher text + // calculated with encryptionKey, and plain-text (above) + // e.g. to generate the desired cipher text + // echo -n hello world | \ + // openssl enc -e -aes-256-cbc -md sha256 \ + // -K '02c735a87529f1d1eb3853852c2a45cf667331dda269c18feac9aec29675b349' \ + // -iv '11111111111111111111111111111111' | \ + // xxd -ps + // # d7cb90a5663df56da49220c09c08f3b9 + 0xD7, 0xCB, 0x90, 0xA5, 0x66, 0x3D, 0xF5, 0x6D, + 0xA4, 0x92, 0x20, 0xC0, 0x9C, 0x08, 0xF3, 0xB9, + ]), + ); + // console.log(buffer.data); + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, value); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + + it('should write encrypted varchar with code page', async () => { + const baseTypeInfo = { + userType: 0x00, + type: dataTypeByName.VarChar, + collation: { codepage: 'WINDOWS-1252' }, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = 'abcdé'; + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from(value, 'ascii'), + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, value); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write nvarchar', async () => { const colMetadata = [{ type: dataTypeByName.NVarChar }]; const value = 'abc'; @@ -250,6 +619,42 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted nvarchar', async () => { + const baseTypeInfo = { + userType: 0x00, + type: dataTypeByName.NVarChar, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = 'abc'; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xd1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from(value, 'utf16le'), + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, value); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varBinary', async () => { const colMetadata = [{ type: dataTypeByName.VarBinary }]; const value = Buffer.from([0x12, 0x34]); @@ -272,6 +677,42 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varBinary', async () => { + const baseTypeInfo = { + type: dataTypeByName.VarBinary, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([0x12, 0x34]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.deepEqual(token.columns[0].value, value); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write binary', async () => { const colMetadata = [{ type: dataTypeByName.Binary }]; const value = Buffer.from([0x12, 0x34]); @@ -294,6 +735,39 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted binary', async () => { + const baseTypeInfo = { type: dataTypeByName.Binary }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([0x12, 0x34]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.deepEqual(token.columns[0].value, value); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varcharMaxNull', async () => { const colMetadata = [ { @@ -323,6 +797,45 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varcharMaxNull', async () => { + const baseTypeInfo = { + type: dataTypeByName.VarChar, + dataLength: 65535, + collation: { codepage: undefined }, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, null); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varcharMaxUnkownLength', async () => { const colMetadata = [ { @@ -392,6 +905,47 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varcharMaxKnownLength', async () => { + const baseTypeInfo = { + type: dataTypeByName.VarChar, + dataLength: 65535, + collation: { codepage: undefined }, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + // no PLP known length prefix, only PLP_NULL needs to be explicitly specified + // 'abcdef' + 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, 'abcdef'); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varcharmaxWithCodePage', async () => { const colMetadata = [ { @@ -425,6 +979,46 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varcharmaxWithCodePage', async () => { + const baseTypeInfo = { + type: dataTypeByName.VarChar, + dataLength: 65535, + collation: { codepage: 'WINDOWS-1252' }, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + // chunk 1: 'abcdéf' + 0x61, 0x62, 0x63, 0x64, 0xE9, 0x66, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, 'abcdéf'); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varcharMaxKnownLengthWrong', async () => { const colMetadata = [ { @@ -487,6 +1081,44 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted varBinaryMaxNull', async () => { + const baseTypeInfo = { + type: dataTypeByName.VarBinary, + dataLength: 65535, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + const value = Buffer.from([ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]); + + const buffer = new WritableTrackingBuffer(0, 'ascii'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, 1); + assert.strictEqual(token.columns[0].value, null); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write varBinaryMaxUnknownLength', async () => { const colMetadata = [ { @@ -663,6 +1295,188 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted intN variants', async () => { + const baseTypeInfo = [ + { type: IntN, dataLength: 0x00 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x08 }, + { type: IntN, dataLength: 0x01 }, + { type: IntN, dataLength: 0x02 }, + { type: IntN, dataLength: 0x04 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + null, + // 8-length intN is treated as bigint, so they will be strings + '0', + '1', + '-1', + '2', + '-2', + '9223372036854775807', + '-9223372036854775808', + '10', + '100', + '1000', + '10000', + // 1-length, 2-length, and 4-length will end up as numbers + 3, + 4, + 5, + ]; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // null + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 0 + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 1 + Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // -1 + Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 2 + Buffer.from([0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // -2 + Buffer.from([0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 9223372036854775807 + Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // -9223372036854775808 + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 10 + Buffer.from([0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 100 + Buffer.from([0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 1000 + Buffer.from([0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 10000 + Buffer.from([0x10, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 3 + Buffer.from([0x03]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 4 + Buffer.from([0x04, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 5 + Buffer.from([0x05, 0x00, 0x00, 0x00]), + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + // console.log(token); + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); + it('parsing a UniqueIdentifier value when `lowerCaseGuids` option is `false`', async () => { const colMetadata = [ { type: dataTypeByName.UniqueIdentifier }, @@ -711,6 +1525,64 @@ describe('Row Token Parser', () => { }); + it('should write encrypted UniqueIdentifier when `lowerCaseGuids` option is `false`', async () => { + const baseTypeInfo = [ + { type: dataTypeByName.UniqueIdentifier, dataLength: 0x00 }, + { type: dataTypeByName.UniqueIdentifier, dataLength: 0x10 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + null, + '67452301-AB89-EFCD-0123-456789ABCDEF', + ]; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // null + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 67452301-AB89-EFCD-0123-456789ABCDEF + Buffer.from([ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + ]), + ), + ); + // console.log(buffer.data) + + const parserOptions = { + ...alwaysEncryptedOptions, + lowerCaseGuids: false, + }; + const parser = Parser.parseTokens([buffer.data], { lowerCaseGuids: false }, parserOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + + // console.log(token); + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); + it('parsing a UniqueIdentifier value when `lowerCaseGuids` option is `true`', async () => { var colMetadata = [ { type: dataTypeByName.UniqueIdentifier }, @@ -756,6 +1628,63 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted UniqueIdentifier when `lowerCaseGuids` option is `true`', async () => { + const baseTypeInfo = [ + { type: dataTypeByName.UniqueIdentifier, dataLength: 0x00 }, + { type: dataTypeByName.UniqueIdentifier, dataLength: 0x10 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + null, + '67452301-ab89-efcd-0123-456789abcdef', + ]; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // null + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 67452301-ab89-efcd-0123-456789abcdef + Buffer.from([ + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, + ]), + ), + ); + // console.log(buffer.data) + + const parserOptions = { + ...alwaysEncryptedOptions, + lowerCaseGuids: true, + }; + const parser = Parser.parseTokens([buffer.data], { lowerCaseGuids: false }, parserOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); + it('should write floatN', async () => { const colMetadata = [ { type: FloatN }, @@ -798,6 +1727,66 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted floatN variants', async () => { + const baseTypeInfo = [ + { type: FloatN, dataLength: 0x00 }, + { type: FloatN, dataLength: 0x04 }, + { type: FloatN, dataLength: 0x08 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + null, + 9.5, + 9.5, + ]; + + const buffer = new WritableTrackingBuffer(0); + buffer.writeUInt8(0xD1); + // despite having a dataLength, all decrypted float types are 8 bytes long + // (they also do not contain the leading dataLength byte) + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 9.5 + Buffer.from([0x00, 0x00, 0x18, 0x41]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + // 9.5 + Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x40]), + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + // console.log(token); + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); + it('should write datetime', async () => { const colMetadata = [{ type: dataTypeByName.DateTime }]; @@ -848,6 +1837,106 @@ describe('Row Token Parser', () => { } }); + it('should write encrypted datetime when `useUTC` option is `false`', async () => { + const baseTypeInfo = { + type: dataTypeByName.DateTime, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + + const value = Buffer.from([ + // January 3, 1900 00:00:45 + // 3rd January 1900 + 0x02, 0x00, 0x00, 0x00, + // 45 seconds + 0xBC, 0x34, 0x00, 0x00 + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parserOptions = { + ...alwaysEncryptedOptions, + useUTC: false, + }; + const parser = Parser.parseTokens([buffer.data], { lowerCaseGuids: false }, parserOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + + assert.strictEqual(token.columns.length, 1); + assert.strictEqual( + token.columns[0].value.getTime(), + new Date('January 3, 1900 00:00:45').getTime(), + ); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + + it('should write encrypted datetime when `useUTC` option is `true`', async () => { + const baseTypeInfo = { + type: dataTypeByName.DateTime, + }; + const colMetadata = [{ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + }]; + + const value = Buffer.from([ + // January 3, 1900 00:00:45 + // 3rd January 1900 + 0x02, 0x00, 0x00, 0x00, + // 45 seconds + 0xBC, 0x34, 0x00, 0x00 + ]); + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + value, + ), + ); + // console.log(buffer.data) + + const parserOptions = { + ...alwaysEncryptedOptions, + useUTC: true, + }; + const parser = Parser.parseTokens([buffer.data], { lowerCaseGuids: false }, parserOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + + assert.strictEqual(token.columns.length, 1); + assert.strictEqual( + token.columns[0].value.getTime(), + new Date('January 3, 1900 00:00:45 GMT').getTime(), + ); + assert.strictEqual(token.columns[0].metadata, colMetadata[0]); + assert.isTrue((await parser.next()).done); + }); + it('should write datetimeN', async () => { const colMetadata = [{ type: DateTimeN }]; @@ -868,6 +1957,68 @@ describe('Row Token Parser', () => { assert.isTrue((await parser.next()).done); }); + it('should write encrypted datetimeN variants', async () => { + const baseTypeInfo = [ + { type: DateTimeN, dataLength: 0x00 }, + { type: DateTimeN, dataLength: 0x04 }, + { type: DateTimeN, dataLength: 0x08 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + null, + new Date('January 3, 1900 00:45:00').getTime(), + new Date('January 3, 1900 00:00:45').getTime(), + ]; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x02, 0x00, 0x2D, 0x00]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([0x02, 0x00, 0x00, 0x00, 0xBC, 0x34, 0x00, 0x00]), + ), + ); + // console.log(buffer.data) + + const parserOptions = { + ...alwaysEncryptedOptions, + useUTC: false, + }; + const parser = Parser.parseTokens([buffer.data], { lowerCaseGuids: false }, parserOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map( + ({ value }) => { return value === null ? value : value.getTime(); }, + ); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); + it('should write numeric4Bytes', async () => { const colMetadata = [ { @@ -1053,4 +2204,109 @@ describe('Row Token Parser', () => { assert.strictEqual(token.columns[0].value, null); assert.isTrue((await parser.next()).done); }); + + it('should write encrypted numericN variants', async () => { + const baseTypeInfo = [ + { type: NumericN, precision: 3, scale: 1, dataLength: 5 }, + { type: NumericN, precision: 3, scale: 1, dataLength: 5 }, + { type: NumericN, precision: 13, scale: 1, dataLength: 9 }, + { type: NumericN, precision: 23, scale: 1, dataLength: 13 }, + { type: NumericN, precision: 33, scale: 1, dataLength: 17 }, + { type: NumericN, precision: 3, scale: 1, dataLength: 0 }, + ]; + const colMetadata = baseTypeInfo.map((baseTypeInfo) => ({ + type: dataTypeByName.VarBinary, + cryptoMetadata: { + ...cryptoMetadata, + baseTypeInfo, + }, + })); + + const expectedValues = [ + 9.3, + -9.3, + (0x100000000 + 93) / 10, + (0x100000000 * 0x100000000 + 0x200000000 + 93) / 10, + (0x100000000 * 0x100000000 * 0x100000000 + + 0x200000000 * 0x100000000 + + 0x300000000 + + 93) / 10, + null, + ]; + + const buffer = new WritableTrackingBuffer(0, 'ucs2'); + buffer.writeUInt8(0xD1); + // despite having a dataLength, all decrypted numeric types have multiples of 8 byte length + // (they also do not contain the leading dataLength byte, but do contain the sign byte) + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([ + // 9.3 + 0x01, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([ + // -9.3 + 0x00, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([ + // 429496738.9 + 0x01, 0x5D, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + ]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([ + // 1844674408229948700 + 0x01, 0x5D, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([ + // 7.922816255115783e+27 + 0x01, 0x5D, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, + ]), + ), + ); + buffer.writeUsVarbyte( + generateEncryptedVarBinary( + alwaysEncryptedCEK, + alwaysEncryptedIV, + Buffer.from([]), + ), + ); + // console.log(buffer.data) + + const parser = Parser.parseTokens([buffer.data], {}, alwaysEncryptedOptions, colMetadata); + const result = await parser.next(); + assert.isFalse(result.done); + const token = result.value; + + assert.strictEqual(token.columns.length, expectedValues.length); + const actualValues = token.columns.map(({ value }) => value); + assert.deepEqual(actualValues, expectedValues); + assert.isTrue((await parser.next()).done); + }); });