Skip to content

Commit e095155

Browse files
authored
fix(sdk): authenticate empty AES-GCM payloads in portable runtimes (#574)
1 parent 4bd9000 commit e095155

12 files changed

Lines changed: 142 additions & 111 deletions

File tree

‎docs/packages/sdk/bsv-sdk.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ id: bsv-sdk
33
title: '@bsv/sdk'
44
kind: package
55
domain: sdk
6-
version: '2.8.0'
6+
version: '2.8.1'
77
npm: '@bsv/sdk'
8-
last_updated: '2026-09-21'
9-
last_verified: '2026-09-21'
8+
last_updated: '2026-09-23'
9+
last_verified: '2026-09-23'
1010
review_cadence_days: 30
1111
status: stable
1212
tags: ['sdk', 'crypto', 'transactions']
@@ -15,7 +15,13 @@ repo: 'https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk'
1515

1616
# @bsv/sdk
1717

18-
The current unpublished candidate corrects BUMP offset arithmetic above 32 bits
18+
The 2.8.1 source candidate repairs portable decryption of authenticated empty
19+
AES-GCM plaintext. Browser/mobile and native Node envelopes now interoperate
20+
for empty encrypted fields. Encryption bytes and the full authentication tag
21+
remain unchanged. No ciphertext, account-data or API migration is required;
22+
publication remains a separate protected npm workflow.
23+
24+
Version 2.8.0 corrects BUMP offset arithmetic above 32 bits
1925
through `Number.MAX_SAFE_INTEGER`, preserving existing wire encodings. Root
2026
calculation, extraction, combination and trimming use the same exact numeric
2127
domain; malformed non-integer and unsafe offsets fail explicitly. No consumer

‎docs/reference/package-api-migrations.md‎

Lines changed: 3 additions & 3 deletions
Large diffs are not rendered by default.

‎docs/reference/stack-facts.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ authorized release action.
6262
| overlays | `@bsv/overlay-discovery-services` | `2.2.5` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-discovery-services](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-discovery-services) |
6363
| overlays | `@bsv/overlay-express` | `2.7.2` | node-library | node-cjs, node-esm | node | `>=22` | [packages/overlays/overlay-express](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/overlay-express) |
6464
| overlays | `@bsv/overlay-topics` | `1.8.4` | node-library | node-esm | node | `>=22` | [packages/overlays/topics](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/overlays/topics) |
65-
| sdk | `@bsv/sdk` | `2.8.0` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) |
65+
| sdk | `@bsv/sdk` | `2.8.1` | browser-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global | browser, node, umd | `>=22` | [packages/sdk](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/sdk) |
6666
| sdk | `@bsv/verifast` | `0.3.6` | wasm-library | browser-bundler, browser-esm, node-cjs, node-esm, umd-global, wasm-worker | browser, node, umd, wasm, worker | `>=22` | [packages/verifast](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/verifast) |
6767
| wallet | `@bsv/btms` | `1.2.3` | node-library | node-cjs, node-esm | node | `>=22` | [packages/wallet/btms](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms) |
6868
| wallet | `@bsv/btms-permission-module` | `1.2.1` | node-library | node-esm | node | `>=22` | [packages/wallet/btms-permission-module](https://github.com/bsv-blockchain/ts-stack/tree/main/packages/wallet/btms-permission-module) |

‎governance/package-release-notes.json‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@
167167
{
168168
"name": "@bsv/sdk",
169169
"publishedVersion": "2.8.0",
170-
"releaseType": "none",
171-
"summary": "Adds TOTP.generateSecure and TOTP.validateSecure for conventional six-digit zero-padded codes while retaining the published legacy methods, and hardens authenticated identity binding, replay state, transaction framing, BEEF ownership, registry payloads, transport deadlines, script-verifier registration, wallet-result ownership, certificate acquisition, and signing context. Corrects empty authenticated HTTP response bodies to use the BRC-104 -1 length sentinel, restoring verification of conforming bodyless responses.",
172-
"migration": "Existing TOTP.generate and TOTP.validate calls retain their historical two-digit, unpadded behavior and require no wire migration. New authentication flows should use generateSecure and validateSecure and store or transmit the six-character code as a string so leading zeroes are preserved. Ordinary valid BEEF, BRC-103 v0.1 peers, and public APIs remain compatible; malformed, ambiguous, oversized, identity-mismatched, or value-creating results now fail closed. Validated wallet results retain ordinary object behavior but are returned as owned value snapshots, so callers must not rely on object, array, or byte-buffer identity with the wallet adapter's raw response. Historical numeric-key JSON objects are recovered as bytes only for documented HTTP wallet byte fields; opaque numeric-key metadata remains an object. Deferred signableTransaction results may remain partial, and completed createAction results may use source values from the caller's immutable inputBEEF. Custom wallets must include direct source transactions for every other completed createAction or signAction input; duplicate input outpoints and unresolved or zero-input value-creating completed results are rejected. Browser applications that require DNS rebinding resistance must use a trusted egress proxy. The response-encoding correction is included in the existing unpublished 2.8.0 candidate. Conforming servers and non-empty response bytes require no migration; non-conforming servers signing zero for an empty response must use the BRC-104 -1 sentinel."
170+
"releaseType": "patch",
171+
"summary": "Fixes portable AES-GCM decryption of valid authenticated empty plaintext, including empty encrypted wallet fields. Preserves historical encryption bytes and full tag verification, and adds native/portable compatibility and negative tests for all supported AES key sizes, IV lengths, block boundaries and SymmetricKey envelopes. SDK 2.8.1 is a source candidate until protected npm publication.",
172+
"migration": "None. Public APIs, ciphertext bytes and stored account data are unchanged. Upgrade browser/mobile consumers to SDK 2.8.1 after publication to decrypt valid empty fields. Missing, truncated or altered authentication tags and wrong keys or IVs still fail closed."
173173
},
174174
{
175175
"name": "@bsv/simple",

‎governance/repository-health/baselines.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@
322322
"@bsv/overlay-discovery-services": "2.2.5",
323323
"@bsv/overlay-express": "2.7.2",
324324
"@bsv/overlay-topics": "1.8.4",
325-
"@bsv/sdk": "2.8.0",
325+
"@bsv/sdk": "2.8.1",
326326
"@bsv/verifast": "0.3.6",
327327
"@bsv/btms": "1.2.3",
328328
"@bsv/btms-permission-module": "1.2.1",

‎packages/sdk/CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ All notable changes to this project will be documented in this file. The format
214214

215215
## [Unreleased]
216216

217+
### 2.8.1 candidate — portable authenticated empty fields
218+
219+
- Fix portable AES-GCM decryption of valid authenticated empty plaintext. This
220+
restores native/browser/mobile interoperability for empty encrypted fields.
221+
- Preserve historical encryption bytes and full authentication-tag validation;
222+
add independent native-oracle tests across AES key sizes, IV lengths and block
223+
boundaries, plus tampering and forced-portable SymmetricKey coverage.
224+
- Migration: none. Existing accounts, ciphertexts and public APIs are unchanged.
225+
Publication remains subject to the protected npm workflow.
226+
217227
### 2.8.0 candidate — authenticated boundaries and additive secure TOTP APIs
218228

219229
- Correct empty authenticated HTTP response preimages to use the BRC-104 `-1`

‎packages/sdk/README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ emits a portable `number[]` settlement artifact so HTTP, WebSocket, Message Box,
1616
and JSON transports preserve identical transaction bytes. The same boundary
1717
protects overlay lookup queries and JSON BEEF responses.
1818

19-
The unpublished 2.8.0 candidate verifies bodyless authenticated HTTP responses
19+
Version 2.8.0 verifies bodyless authenticated HTTP responses
2020
using the BRC-104 `-1` body-length sentinel. Conforming 204 and empty error
2121
responses now verify; non-empty response encoding is unchanged. Servers that
2222
sign a zero body length for an empty response must adopt the specified sentinel.
@@ -45,6 +45,13 @@ in UMD; their reviewed ceilings are 742,500 and 556,000 bytes respectively.
4545
The combined sync and security candidate measures 560,560 raw bytes with esbuild;
4646
its reviewed raw ceiling is 561,000 bytes. Compression ceilings are unchanged.
4747

48+
The 2.8.1 candidate fixes portable AES-GCM decryption of authenticated empty
49+
plaintext, including empty encrypted wallet fields. Browser/mobile runtimes and
50+
Node now accept the same valid envelopes. Encryption bytes and full 16-byte tag
51+
verification are unchanged; invalid tags, keys and IVs remain rejected. No API,
52+
wire, account-data or ciphertext migration is required. This source candidate is
53+
not published until the protected npm release workflow completes.
54+
4855
## Table of Contents
4956

5057
1. [Objective](#objective)

‎packages/sdk/docs/reference/primitives.md‎

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5389,51 +5389,13 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
53895389
---
53905390
### Function: AESGCM
53915391

5392-
SECURITY NOTE – NON-STANDARD AES-GCM PADDING
5393-
5394-
This implementation intentionally deviates from NIST SP 800-38D’s AES-GCM
5395-
specification in how the GHASH input is formed when the additional
5396-
authenticated data (AAD) or ciphertext length is zero.
5397-
5398-
In the standard, AAD and ciphertext are each padded with the minimum number
5399-
of zero bytes required to reach a multiple of 16 bytes; when the length is
5400-
already a multiple of 16 (including the case length = 0), no padding block
5401-
is added. In this implementation, when AAD.length === 0 or ciphertext.length
5402-
=== 0, an extra 16-byte block of zeros is appended before the length fields
5403-
are processed. The same formatting logic is used symmetrically in both
5404-
AESGCM (encryption) and AESGCMDecrypt (decryption).
5405-
5406-
As a result:
5407-
- Authentication tags produced here are NOT compatible with tags produced
5408-
by standards-compliant AES-GCM implementations in the cases where AAD
5409-
or ciphertext are empty.
5410-
- Ciphertexts generated by this code must be decrypted by this exact
5411-
implementation (or one that reproduces the same GHASH formatting), and
5412-
must not be mixed with ciphertexts produced by a strictly standard
5413-
AES-GCM library.
5414-
5415-
Cryptographic impact: this change alters only the encoding of the message
5416-
that is input to GHASH; it does not change the block cipher, key derivation,
5417-
IV handling, or the basic “encrypt-then-MAC over (AAD, ciphertext, lengths)”
5418-
structure of AES-GCM. Under the usual assumptions that AES is a secure block
5419-
cipher and GHASH with a secret subkey is a secure polynomial MAC, this
5420-
variant continues to provide confidentiality and integrity for data encrypted
5421-
and decrypted consistently with this implementation. We are not aware of any
5422-
attack that exploits the presence of this extra zero block when AAD or
5423-
ciphertext are empty.
5424-
5425-
However, this padding behavior is non-compliant with NIST SP 800-38D and has
5426-
not been analyzed as extensively as standard AES-GCM. Code that requires
5427-
strict standards compliance or interoperability with external AES-GCM
5428-
implementations SHOULD NOT use this module as-is. Any future migration to a
5429-
fully compliant AES-GCM encoding will require a compatibility strategy, as
5430-
existing ciphertexts produced by this implementation will otherwise become
5431-
undecryptable.
5432-
5433-
This non-standard padding behavior is retained intentionally for backward
5434-
compatibility: existing ciphertexts in production were generated with this
5435-
encoding, and changing it would render previously encrypted data
5436-
undecryptable by newer versions of the library.
5392+
AES-GCM without additional authenticated data.
5393+
5394+
Keep the historical GHASH input formatting in buildAuthInput unchanged.
5395+
Its leading zero block (and extra zero block for empty ciphertext) does not
5396+
change the tag: GHASH starts at zero, and processing a zero block leaves it
5397+
at zero. Native interoperability tests cover empty and block-boundary inputs.
5398+
Empty ciphertext is valid but still requires the complete, matching tag.
54375399

54385400
```ts
54395401
export function AESGCM(plainText: Bytes, initializationVector: Bytes, key: Bytes): {

‎packages/sdk/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": "2.8.0",
3+
"version": "2.8.1",
44
"sideEffects": false,
55
"engines": {
66
"node": ">=22"

‎packages/sdk/src/primitives/AESGCM.ts‎

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -444,51 +444,13 @@ function buildAuthInput (cipherText: Bytes): Bytes {
444444
}
445445

446446
/**
447-
* SECURITY NOTE – NON-STANDARD AES-GCM PADDING
447+
* AES-GCM without additional authenticated data.
448448
*
449-
* This implementation intentionally deviates from NIST SP 800-38D’s AES-GCM
450-
* specification in how the GHASH input is formed when the additional
451-
* authenticated data (AAD) or ciphertext length is zero.
452-
*
453-
* In the standard, AAD and ciphertext are each padded with the minimum number
454-
* of zero bytes required to reach a multiple of 16 bytes; when the length is
455-
* already a multiple of 16 (including the case length = 0), no padding block
456-
* is added. In this implementation, when AAD.length === 0 or ciphertext.length
457-
* === 0, an extra 16-byte block of zeros is appended before the length fields
458-
* are processed. The same formatting logic is used symmetrically in both
459-
* AESGCM (encryption) and AESGCMDecrypt (decryption).
460-
*
461-
* As a result:
462-
* - Authentication tags produced here are NOT compatible with tags produced
463-
* by standards-compliant AES-GCM implementations in the cases where AAD
464-
* or ciphertext are empty.
465-
* - Ciphertexts generated by this code must be decrypted by this exact
466-
* implementation (or one that reproduces the same GHASH formatting), and
467-
* must not be mixed with ciphertexts produced by a strictly standard
468-
* AES-GCM library.
469-
*
470-
* Cryptographic impact: this change alters only the encoding of the message
471-
* that is input to GHASH; it does not change the block cipher, key derivation,
472-
* IV handling, or the basic “encrypt-then-MAC over (AAD, ciphertext, lengths)”
473-
* structure of AES-GCM. Under the usual assumptions that AES is a secure block
474-
* cipher and GHASH with a secret subkey is a secure polynomial MAC, this
475-
* variant continues to provide confidentiality and integrity for data encrypted
476-
* and decrypted consistently with this implementation. We are not aware of any
477-
* attack that exploits the presence of this extra zero block when AAD or
478-
* ciphertext are empty.
479-
*
480-
* However, this padding behavior is non-compliant with NIST SP 800-38D and has
481-
* not been analyzed as extensively as standard AES-GCM. Code that requires
482-
* strict standards compliance or interoperability with external AES-GCM
483-
* implementations SHOULD NOT use this module as-is. Any future migration to a
484-
* fully compliant AES-GCM encoding will require a compatibility strategy, as
485-
* existing ciphertexts produced by this implementation will otherwise become
486-
* undecryptable.
487-
*
488-
* This non-standard padding behavior is retained intentionally for backward
489-
* compatibility: existing ciphertexts in production were generated with this
490-
* encoding, and changing it would render previously encrypted data
491-
* undecryptable by newer versions of the library.
449+
* Keep the historical GHASH input formatting in buildAuthInput unchanged.
450+
* Its leading zero block (and extra zero block for empty ciphertext) does not
451+
* change the tag: GHASH starts at zero, and processing a zero block leaves it
452+
* at zero. Native interoperability tests cover empty and block-boundary inputs.
453+
* Empty ciphertext is valid but still requires the complete, matching tag.
492454
*/
493455
export function AESGCM (
494456
plainText: Bytes,
@@ -551,10 +513,6 @@ export function AESGCMDecrypt (
551513
authenticationTag: Bytes,
552514
key: Bytes
553515
): Bytes | null {
554-
if (cipherText.length === 0) {
555-
throw new Error('Cipher text must not be empty')
556-
}
557-
558516
if (initializationVector.length === 0) {
559517
throw new Error('Initialization vector must not be empty')
560518
}

0 commit comments

Comments
 (0)