@@ -34,21 +34,78 @@ import { createHash, randomBytes, createCipheriv, createDecipheriv } from 'node:
3434 * folded into AES-GCM AAD so a ciphertext rewrapped from a different
3535 * (ns, key) tuple fails decryption — guards against operators
3636 * accidentally copying rows between namespaces.
37+ *
38+ * WebContainer (StackBlitz) note: `node:crypto.createCipheriv('aes-256-gcm', …)`
39+ * is not implemented in WebContainer. When we detect that runtime, we
40+ * swap to a pure-JS AES-GCM from `@noble/ciphers/aes.js`, producing the
41+ * same `iv || tag || ciphertext` byte layout so the handle shape is
42+ * unchanged. The swap is best-effort: if the dependency is missing,
43+ * we fall back to the Node implementation and let it throw, surfacing
44+ * the configuration problem clearly.
3745 */
46+ const isWebContainerRuntime = ( ) : boolean => {
47+ const g = globalThis as any ;
48+ return (
49+ typeof g !== 'undefined' &&
50+ ( Boolean ( g . process ?. versions ?. webcontainer ) ||
51+ Boolean ( g . process ?. env ?. SHELL ?. includes ?.( 'jsh' ) ) ||
52+ Boolean ( g . process ?. env ?. STACKBLITZ ) )
53+ ) ;
54+ } ;
55+
56+ type GcmFactory = ( key : Uint8Array , nonce : Uint8Array , aad ?: Uint8Array ) => {
57+ encrypt : ( plain : Uint8Array ) => Uint8Array ;
58+ decrypt : ( cipher : Uint8Array ) => Uint8Array ;
59+ } ;
60+
61+ let nobleGcmPromise : Promise < GcmFactory | undefined > | undefined ;
62+ const loadNobleGcm = ( ) : Promise < GcmFactory | undefined > => {
63+ if ( ! nobleGcmPromise ) {
64+ nobleGcmPromise = ( async ( ) => {
65+ try {
66+ const mod = await import ( '@noble/ciphers/aes.js' ) ;
67+ return mod . gcm as unknown as GcmFactory ;
68+ } catch ( err : any ) {
69+ console . warn (
70+ `[InMemoryCryptoProvider] WebContainer detected but @noble/ciphers not installed: ${ err ?. message ?? err } . Falling back to node:crypto (will throw).` ,
71+ ) ;
72+ return undefined ;
73+ }
74+ } ) ( ) ;
75+ }
76+ return nobleGcmPromise ;
77+ } ;
78+
3879export class InMemoryCryptoProvider implements ICryptoProvider {
3980 private readonly key : Buffer ;
81+ private readonly useNoble : boolean ;
4082
4183 constructor ( opts : { key ?: Buffer } = { } ) {
4284 this . key = opts . key ?? randomBytes ( 32 ) ;
85+ this . useNoble = isWebContainerRuntime ( ) ;
4386 }
4487
4588 async encrypt ( plain : string , ctx : CryptoContext ) : Promise < CryptoHandle > {
4689 const iv = randomBytes ( 12 ) ;
47- const cipher = createCipheriv ( 'aes-256-gcm' , this . key , iv ) ;
48- cipher . setAAD ( Buffer . from ( this . aadOf ( ctx ) , 'utf8' ) ) ;
49- const enc = Buffer . concat ( [ cipher . update ( plain , 'utf8' ) , cipher . final ( ) ] ) ;
50- const tag = cipher . getAuthTag ( ) ;
51- const blob = Buffer . concat ( [ iv , tag , enc ] ) . toString ( 'base64' ) ;
90+ const aad = Buffer . from ( this . aadOf ( ctx ) , 'utf8' ) ;
91+ const plainBytes = Buffer . from ( plain , 'utf8' ) ;
92+
93+ let blob : string ;
94+ if ( this . useNoble ) {
95+ const gcm = await loadNobleGcm ( ) ;
96+ if ( gcm ) {
97+ const cipher = gcm ( this . key , iv , aad ) ;
98+ const ctWithTag = cipher . encrypt ( plainBytes ) ; // ciphertext || tag(16)
99+ const ct = ctWithTag . subarray ( 0 , ctWithTag . length - 16 ) ;
100+ const tag = ctWithTag . subarray ( ctWithTag . length - 16 ) ;
101+ blob = Buffer . concat ( [ iv , Buffer . from ( tag ) , Buffer . from ( ct ) ] ) . toString ( 'base64' ) ;
102+ } else {
103+ blob = this . encryptNode ( plainBytes , iv , aad ) ;
104+ }
105+ } else {
106+ blob = this . encryptNode ( plainBytes , iv , aad ) ;
107+ }
108+
52109 return {
53110 id : 'sec_' + randomBytes ( 16 ) . toString ( 'hex' ) ,
54111 kmsKeyId : 'local:in-memory:v1' ,
@@ -63,8 +120,19 @@ export class InMemoryCryptoProvider implements ICryptoProvider {
63120 const iv = buf . subarray ( 0 , 12 ) ;
64121 const tag = buf . subarray ( 12 , 28 ) ;
65122 const data = buf . subarray ( 28 ) ;
123+ const aad = Buffer . from ( this . aadOf ( ctx ) , 'utf8' ) ;
124+
125+ if ( this . useNoble ) {
126+ const gcm = await loadNobleGcm ( ) ;
127+ if ( gcm ) {
128+ const cipher = gcm ( this . key , iv , aad ) ;
129+ const ctWithTag = Buffer . concat ( [ data , tag ] ) ; // noble expects ciphertext || tag
130+ const out = cipher . decrypt ( ctWithTag ) ;
131+ return Buffer . from ( out ) . toString ( 'utf8' ) ;
132+ }
133+ }
66134 const decipher = createDecipheriv ( 'aes-256-gcm' , this . key , iv ) ;
67- decipher . setAAD ( Buffer . from ( this . aadOf ( ctx ) , 'utf8' ) ) ;
135+ decipher . setAAD ( aad ) ;
68136 decipher . setAuthTag ( tag ) ;
69137 return Buffer . concat ( [ decipher . update ( data ) , decipher . final ( ) ] ) . toString ( 'utf8' ) ;
70138 }
@@ -84,6 +152,14 @@ export class InMemoryCryptoProvider implements ICryptoProvider {
84152 return 'sha256:' + createHash ( 'sha256' ) . update ( plain , 'utf8' ) . digest ( 'hex' ) ;
85153 }
86154
155+ private encryptNode ( plainBytes : Buffer , iv : Buffer , aad : Buffer ) : string {
156+ const cipher = createCipheriv ( 'aes-256-gcm' , this . key , iv ) ;
157+ cipher . setAAD ( aad ) ;
158+ const enc = Buffer . concat ( [ cipher . update ( plainBytes ) , cipher . final ( ) ] ) ;
159+ const tag = cipher . getAuthTag ( ) ;
160+ return Buffer . concat ( [ iv , tag , enc ] ) . toString ( 'base64' ) ;
161+ }
162+
87163 private aadOf ( ctx : CryptoContext ) : string {
88164 // Bind ciphertext to (namespace,key) so a row cannot be moved across
89165 // specifiers. Tenant binding is intentionally omitted because the
0 commit comments