Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: column encryption parsing support #1613

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/always-encrypted/cek-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,21 @@
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.');

Check warning on line 60 in src/always-encrypted/cek-entry.ts

View check run for this annotation

Codecov / codecov/patch

src/always-encrypted/cek-entry.ts#L60

Added line #L60 was not covered by tests
}
}
}
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 Metadata = {
cryptoMetadata?: CryptoMetadata;
cryptoMetadata?: CryptoMetadata | undefined;
} & BaseMetadata;

function readCollation(buf: Buffer, offset: number): Result<Collation> {
Expand Down Expand Up @@ -113,12 +113,12 @@
}, 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 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);

Check warning on line 362 in src/metadata-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/metadata-parser.ts#L362

Added line #L362 was not covered by tests
} 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 @@
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 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;

Check warning on line 74 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L73-L74

Added lines #L73 - L74 were not covered by tests
}

throw err;

Check warning on line 77 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L77

Added line #L77 was not covered by tests
}

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;

Check warning on line 94 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L93-L94

Added lines #L93 - L94 were not covered by tests
}

throw err;

Check warning on line 97 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L97

Added line #L97 was not covered by tests
}

parser.position = offset;
cekEntries.push(cek);

break;
}
}
return new Result(cekEntries, parser.position);
}
return new Result(undefined, parser.position);

Check warning on line 108 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L108

Added line #L108 was not covered by tests
}


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;

Check warning on line 130 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L129-L130

Added lines #L129 - L130 were not covered by tests
}

throw err;

Check warning on line 133 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L133

Added line #L133 was not covered by tests
}

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;

Check warning on line 155 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L154-L155

Added lines #L154 - L155 were not covered by tests
}

throw err;

Check warning on line 158 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L158

Added line #L158 was not covered by tests
}

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);

Check warning on line 234 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L232-L234

Added lines #L232 - L234 were not covered by tests
}
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 @@
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 @@
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;

Check warning on line 305 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L304-L305

Added lines #L304 - L305 were not covered by tests
}

throw err;

Check warning on line 308 in src/token/colmetadata-token-parser.ts

View check run for this annotation

Codecov / codecov/patch

src/token/colmetadata-token-parser.ts#L308

Added line #L308 was not covered by tests
}

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
Loading