Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

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

if (this.databaseId === 0) {
this.databaseId = encryptionKey.dbId;
this.cekId = encryptionKey.keyId;
this.cekVersion = encryptionKey.keyVersion;
this.cekMdVersion = encryptionKey.mdVersion;
} else if ((this.databaseId !== encryptionKey.dbId) ||
(this.cekId !== encryptionKey.keyId) ||
(this.cekVersion !== encryptionKey.keyVersion) ||
!this.cekMdVersion || !encryptionKey.mdVersion ||
this.cekMdVersion.length !== encryptionKey.mdVersion.length) {
throw new Error('Invalid databaseId, cekId, cekVersion or cekMdVersion.');
}
}
}
2 changes: 1 addition & 1 deletion src/always-encrypted/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface EncryptionAlgorithm {
}

export interface CryptoMetadata {
cekEntry?: CEKEntry;
cekEntry?: CEKEntry | undefined;
cipherAlgorithmId: number;
cipherAlgorithmName?: string;
normalizationRuleVersion: Buffer;
Expand Down
20 changes: 19 additions & 1 deletion src/login7-payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,20 @@ const FEDAUTH_OPTIONS = {

const FEATURE_EXT_TERMINATOR = 0xFF;

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

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

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

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

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

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

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

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

buffers.push(buffer);
}

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

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

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

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

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

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

function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void) {
function metadataParse(parser: Parser, options: ParserOptions, callback: (metadata: Metadata) => void, shouldReadFlags = true) {
(async () => {
while (true) {
let result;
try {
result = readMetadata(parser.buffer, parser.position, options);
result = readMetadata(parser.buffer, parser.position, options, shouldReadFlags);
} catch (err: any) {
if (err instanceof NotEnoughDataError) {
await parser.waitForChunk();
Expand Down
228 changes: 222 additions & 6 deletions src/token/colmetadata-token-parser.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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<string | string[] | undefined> {
if (!metadata.type.hasTableName) {
return new Result(undefined, offset);
Expand Down Expand Up @@ -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<Result<CEKEntry[] | undefined>> {

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<Result<CEKEntry>> {
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<EncryptionKeyInfo> {
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<CryptoMetadata> {
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<string> {
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<ColumnMetadata> {
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));

Expand All @@ -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);
}

Expand All @@ -97,14 +292,35 @@ async function colMetadataParser(parser: Parser): Promise<ColMetadataToken> {
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) {
let column: ColumnMetadata;
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();
Expand Down
Loading