Skip to content

Commit 1e0cc5c

Browse files
authored
Merge pull request #411 from bsv-blockchain/fix/tob-13
TOB-13
2 parents 0b96416 + a6073e9 commit 1e0cc5c

File tree

7 files changed

+218
-300
lines changed

7 files changed

+218
-300
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. The format
55
## Table of Contents
66

77
- [Unreleased](#unreleased)
8+
- [1.9.25 - 2025-12-09](#1925---2025-12-09)
89
- [1.9.24 - 2025-12-09](#1924---2025-12-09)
910
- [1.9.23 - 2025-12-08](#1923---2025-12-08)
1011
- [1.9.22 - 2025-12-05](#1922---2025-12-04)
@@ -197,6 +198,16 @@ All notable changes to this project will be documented in this file. The format
197198

198199
---
199200

201+
## [1.9.25] - 2025-12-09
202+
203+
### Added
204+
- Documentation disclaimer for our specific AESGCM implementation of padding for additional authenticated data (AAD) and ciphertext.
205+
206+
### Removed
207+
- Removed support for additional authenticated data (AAD) padding in AESGCM.
208+
209+
---
210+
200211
## [1.9.24] - 2025-12-09
201212

202213
### Fixed

docs/reference/primitives.md

Lines changed: 139 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4958,20 +4958,16 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
49584958
---
49594959
## Functions
49604960
4961-
| |
4962-
| --- |
4963-
| [AES](#function-aes) |
4964-
| [AESGCM](#function-aesgcm) |
4965-
| [AESGCMDecrypt](#function-aesgcmdecrypt) |
4966-
| [assertValidHex](#function-assertvalidhex) |
4967-
| [base64ToArray](#function-base64toarray) |
4968-
| [ghash](#function-ghash) |
4969-
| [normalizeHex](#function-normalizehex) |
4970-
| [pbkdf2](#function-pbkdf2) |
4971-
| [red](#function-red) |
4972-
| [toArray](#function-toarray) |
4973-
| [toBase64](#function-tobase64) |
4974-
| [verifyNotNull](#function-verifynotnull) |
4961+
| | |
4962+
| --- | --- |
4963+
| [AES](#function-aes) | [pbkdf2](#function-pbkdf2) |
4964+
| [AESGCM](#function-aesgcm) | [realHtonl](#function-realhtonl) |
4965+
| [AESGCMDecrypt](#function-aesgcmdecrypt) | [red](#function-red) |
4966+
| [assertValidHex](#function-assertvalidhex) | [swapBytes32](#function-swapbytes32) |
4967+
| [base64ToArray](#function-base64toarray) | [toArray](#function-toarray) |
4968+
| [ghash](#function-ghash) | [toBase64](#function-tobase64) |
4969+
| [htonl](#function-htonl) | [verifyNotNull](#function-verifynotnull) |
4970+
| [normalizeHex](#function-normalizehex) | |
49754971
49764972
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
49774973
@@ -4988,8 +4984,54 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
49884984
---
49894985
### Function: AESGCM
49904986

4991-
```ts
4992-
export function AESGCM(plainText: number[], additionalAuthenticatedData: number[], initializationVector: number[], key: number[]): {
4987+
SECURITY NOTE – NON-STANDARD AES-GCM PADDING
4988+
4989+
This implementation intentionally deviates from NIST SP 800-38D’s AES-GCM
4990+
specification in how the GHASH input is formed when the additional
4991+
authenticated data (AAD) or ciphertext length is zero.
4992+
4993+
In the standard, AAD and ciphertext are each padded with the minimum number
4994+
of zero bytes required to reach a multiple of 16 bytes; when the length is
4995+
already a multiple of 16 (including the case length = 0), no padding block
4996+
is added. In this implementation, when AAD.length === 0 or ciphertext.length
4997+
=== 0, an extra 16-byte block of zeros is appended before the length fields
4998+
are processed. The same formatting logic is used symmetrically in both
4999+
AESGCM (encryption) and AESGCMDecrypt (decryption).
5000+
5001+
As a result:
5002+
- Authentication tags produced here are NOT compatible with tags produced
5003+
by standards-compliant AES-GCM implementations in the cases where AAD
5004+
or ciphertext are empty.
5005+
- Ciphertexts generated by this code must be decrypted by this exact
5006+
implementation (or one that reproduces the same GHASH formatting), and
5007+
must not be mixed with ciphertexts produced by a strictly standard
5008+
AES-GCM library.
5009+
5010+
Cryptographic impact: this change alters only the encoding of the message
5011+
that is input to GHASH; it does not change the block cipher, key derivation,
5012+
IV handling, or the basicencrypt-then-MAC over (AAD, ciphertext, lengths)”
5013+
structure of AES-GCM. Under the usual assumptions that AES is a secure block
5014+
cipher and GHASH with a secret subkey is a secure polynomial MAC, this
5015+
variant continues to provide confidentiality and integrity for data encrypted
5016+
and decrypted consistently with this implementation. We are not aware of any
5017+
attack that exploits the presence of this extra zero block when AAD or
5018+
ciphertext are empty.
5019+
5020+
However, this padding behavior is non-compliant with NIST SP 800-38D and has
5021+
not been analyzed as extensively as standard AES-GCM. Code that requires
5022+
strict standards compliance or interoperability with external AES-GCM
5023+
implementations SHOULD NOT use this module as-is. Any future migration to a
5024+
fully compliant AES-GCM encoding will require a compatibility strategy, as
5025+
existing ciphertexts produced by this implementation will otherwise become
5026+
undecryptable.
5027+
5028+
This non-standard padding behavior is retained intentionally for backward
5029+
compatibility: existing ciphertexts in production were generated with this
5030+
encoding, and changing it would render previously encrypted data
5031+
undecryptable by newer versions of the library.
5032+
5033+
```ts
5034+
export function AESGCM(plainText: number[], initializationVector: number[], key: number[]): {
49935035
result: number[];
49945036
authenticationTag: number[];
49955037
}
@@ -5001,7 +5043,7 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
50015043
### Function: AESGCMDecrypt
50025044

50035045
```ts
5004-
export function AESGCMDecrypt(cipherText: number[], additionalAuthenticatedData: number[], initializationVector: number[], authenticationTag: number[], key: number[]): number[] | null
5046+
export function AESGCMDecrypt(cipherText: number[], initializationVector: number[], authenticationTag: number[], key: number[]): number[] | null
50055047
```
50065048

50075049
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
@@ -5033,6 +5075,15 @@ export function ghash(input: number[], hashSubKey: number[]): number[]
50335075

50345076
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
50355077

5078+
---
5079+
### Function: htonl
5080+
5081+
```ts
5082+
export function htonl(w: number): number
5083+
```
5084+
5085+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5086+
50365087
---
50375088
### Function: normalizeHex
50385089

@@ -5070,6 +5121,42 @@ Argument Details
50705121

50715122
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
50725123

5124+
---
5125+
### Function: realHtonl
5126+
5127+
Converts a 32-bit unsigned integer from host byte order to network byte order.
5128+
5129+
Unlike the legacy `htonl()` implementation (which always swapped bytes),
5130+
this function behaves like the traditional C `htonl()`:
5131+
5132+
- On **little-endian** machines → performs a byte swap.
5133+
- On **big-endian** machines → returns the value unchanged.
5134+
5135+
This function is provided to resolve TOB-20, which identified that the
5136+
previous `htonl()` implementation had a misleading name and did not match
5137+
platform-dependent semantics.
5138+
5139+
Example
5140+
5141+
```ts
5142+
realHtonl(0x11223344) // → 0x44332211 on little-endian systems
5143+
```
5144+
5145+
```ts
5146+
export function realHtonl(w: number): number
5147+
```
5148+
5149+
Returns
5150+
5151+
The value converted to network byte order.
5152+
5153+
Argument Details
5154+
5155+
+ **w**
5156+
+ A 32-bit unsigned integer.
5157+
5158+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5159+
50735160
---
50745161
### Function: red
50755162

@@ -5079,6 +5166,41 @@ export function red(x: bigint): bigint
50795166

50805167
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
50815168

5169+
---
5170+
### Function: swapBytes32
5171+
5172+
Unconditionally swaps the byte order of a 32-bit unsigned integer.
5173+
5174+
This function performs a strict 32-bit byte swap regardless of host
5175+
endianness. It is equivalent to the behavior commonly referred to as
5176+
`bswap32` in low-level libraries.
5177+
5178+
This function is introduced as part of TOB-20 to provide a clearly-named
5179+
alternative to `htonl()`, which was previously implemented as an
5180+
unconditional byte swap and did not match the semantics of the traditional
5181+
C `htonl()` function.
5182+
5183+
Example
5184+
5185+
```ts
5186+
swapBytes32(0x11223344) // → 0x44332211
5187+
```
5188+
5189+
```ts
5190+
export function swapBytes32(w: number): number
5191+
```
5192+
5193+
Returns
5194+
5195+
The value with its byte order reversed.
5196+
5197+
Argument Details
5198+
5199+
+ **w**
5200+
+ A 32-bit unsigned integer.
5201+
5202+
Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
5203+
50825204
---
50835205
### Function: toArray
50845206

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bsv/sdk",
3-
"version": "1.9.24",
3+
"version": "1.9.25",
44
"type": "module",
55
"description": "BSV Blockchain Software Development Kit",
66
"main": "dist/cjs/mod.js",

src/primitives/AESGCM.ts

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,55 @@ function gctr (
323323
return output
324324
}
325325

326+
/**
327+
* SECURITY NOTE – NON-STANDARD AES-GCM PADDING
328+
*
329+
* This implementation intentionally deviates from NIST SP 800-38D’s AES-GCM
330+
* specification in how the GHASH input is formed when the additional
331+
* authenticated data (AAD) or ciphertext length is zero.
332+
*
333+
* In the standard, AAD and ciphertext are each padded with the minimum number
334+
* of zero bytes required to reach a multiple of 16 bytes; when the length is
335+
* already a multiple of 16 (including the case length = 0), no padding block
336+
* is added. In this implementation, when AAD.length === 0 or ciphertext.length
337+
* === 0, an extra 16-byte block of zeros is appended before the length fields
338+
* are processed. The same formatting logic is used symmetrically in both
339+
* AESGCM (encryption) and AESGCMDecrypt (decryption).
340+
*
341+
* As a result:
342+
* - Authentication tags produced here are NOT compatible with tags produced
343+
* by standards-compliant AES-GCM implementations in the cases where AAD
344+
* or ciphertext are empty.
345+
* - Ciphertexts generated by this code must be decrypted by this exact
346+
* implementation (or one that reproduces the same GHASH formatting), and
347+
* must not be mixed with ciphertexts produced by a strictly standard
348+
* AES-GCM library.
349+
*
350+
* Cryptographic impact: this change alters only the encoding of the message
351+
* that is input to GHASH; it does not change the block cipher, key derivation,
352+
* IV handling, or the basic “encrypt-then-MAC over (AAD, ciphertext, lengths)”
353+
* structure of AES-GCM. Under the usual assumptions that AES is a secure block
354+
* cipher and GHASH with a secret subkey is a secure polynomial MAC, this
355+
* variant continues to provide confidentiality and integrity for data encrypted
356+
* and decrypted consistently with this implementation. We are not aware of any
357+
* attack that exploits the presence of this extra zero block when AAD or
358+
* ciphertext are empty.
359+
*
360+
* However, this padding behavior is non-compliant with NIST SP 800-38D and has
361+
* not been analyzed as extensively as standard AES-GCM. Code that requires
362+
* strict standards compliance or interoperability with external AES-GCM
363+
* implementations SHOULD NOT use this module as-is. Any future migration to a
364+
* fully compliant AES-GCM encoding will require a compatibility strategy, as
365+
* existing ciphertexts produced by this implementation will otherwise become
366+
* undecryptable.
367+
*
368+
* This non-standard padding behavior is retained intentionally for backward
369+
* compatibility: existing ciphertexts in production were generated with this
370+
* encoding, and changing it would render previously encrypted data
371+
* undecryptable by newer versions of the library.
372+
*/
326373
export function AESGCM (
327374
plainText: number[],
328-
additionalAuthenticatedData: number[],
329375
initializationVector: number[],
330376
key: number[]
331377
): { result: number[], authenticationTag: number[] } {
@@ -338,7 +384,7 @@ export function AESGCM (
338384
}
339385

340386
let preCounterBlock
341-
let plainTag
387+
let plainTag: number[] = []
342388
const hashSubKey = AES(createZeroBlock(16), key)
343389
preCounterBlock = [...initializationVector]
344390
if (initializationVector.length === 12) {
@@ -358,14 +404,7 @@ export function AESGCM (
358404

359405
const cipherText = gctr(plainText, incrementLeastSignificantThirtyTwoBits(preCounterBlock), key)
360406

361-
plainTag = additionalAuthenticatedData.slice()
362-
363-
if (additionalAuthenticatedData.length === 0) {
364-
plainTag = plainTag.concat(createZeroBlock(16))
365-
} else if (additionalAuthenticatedData.length % 16 !== 0) {
366-
plainTag = plainTag.concat(createZeroBlock(16 - (additionalAuthenticatedData.length % 16)))
367-
}
368-
407+
plainTag = plainTag.concat(createZeroBlock(16))
369408
plainTag = plainTag.concat(cipherText)
370409

371410
if (cipherText.length === 0) {
@@ -375,7 +414,7 @@ export function AESGCM (
375414
}
376415

377416
plainTag = plainTag.concat(createZeroBlock(4))
378-
.concat(getBytes(additionalAuthenticatedData.length * 8))
417+
.concat(getBytes(0))
379418
.concat(createZeroBlock(4)).concat(getBytes(cipherText.length * 8))
380419

381420
return {
@@ -386,7 +425,6 @@ export function AESGCM (
386425

387426
export function AESGCMDecrypt (
388427
cipherText: number[],
389-
additionalAuthenticatedData: number[],
390428
initializationVector: number[],
391429
authenticationTag: number[],
392430
key: number[]
@@ -404,7 +442,7 @@ export function AESGCMDecrypt (
404442
}
405443

406444
let preCounterBlock
407-
let compareTag
445+
let compareTag: number[] = []
408446

409447
// Generate the hash subkey
410448
const hashSubKey = AES(createZeroBlock(16), key)
@@ -425,14 +463,7 @@ export function AESGCMDecrypt (
425463
// Decrypt to obtain the plain text
426464
const plainText = gctr(cipherText, incrementLeastSignificantThirtyTwoBits(preCounterBlock), key)
427465

428-
compareTag = additionalAuthenticatedData.slice()
429-
430-
if (additionalAuthenticatedData.length === 0) {
431-
compareTag = compareTag.concat(createZeroBlock(16))
432-
} else if (additionalAuthenticatedData.length % 16 !== 0) {
433-
compareTag = compareTag.concat(createZeroBlock(16 - (additionalAuthenticatedData.length % 16)))
434-
}
435-
466+
compareTag = compareTag.concat(createZeroBlock(16))
436467
compareTag = compareTag.concat(cipherText)
437468

438469
if (cipherText.length === 0) {
@@ -442,8 +473,9 @@ export function AESGCMDecrypt (
442473
}
443474

444475
compareTag = compareTag.concat(createZeroBlock(4))
445-
.concat(getBytes(additionalAuthenticatedData.length * 8))
446-
.concat(createZeroBlock(4)).concat(getBytes(cipherText.length * 8))
476+
.concat(getBytes(0))
477+
.concat(createZeroBlock(4))
478+
.concat(getBytes(cipherText.length * 8))
447479

448480
// Generate the authentication tag
449481
const calculatedTag = gctr(ghash(compareTag, hashSubKey), preCounterBlock, key)

src/primitives/SymmetricKey.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class SymmetricKey extends BigNumber {
4444
const iv = Random(32)
4545
msg = toArray(msg, enc)
4646
const keyBytes = this.toArray('be', 32)
47-
const { result, authenticationTag } = AESGCM(msg, [], iv, keyBytes)
47+
const { result, authenticationTag } = AESGCM(msg, iv, keyBytes)
4848
const totalLength = iv.length + result.length + authenticationTag.length
4949
const combined = new Array(totalLength)
5050
let offset = 0
@@ -89,7 +89,6 @@ export default class SymmetricKey extends BigNumber {
8989

9090
const result = AESGCMDecrypt(
9191
ciphertext,
92-
[],
9392
iv,
9493
messageTag,
9594
this.toArray('be', 32)

0 commit comments

Comments
 (0)