npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@generazioneai/crypto

v0.9.0

Published

Cifratura a busta dei segreti nei database (ADR-012): formato unico gx2 con AAD obbligatoria, DEK per servizio e azienda cifrate da una KEK nel KMS (Infisical KMS o chiave locale), lettura dei formati di oggi; KEK di emergenza fuori linea (secondo involuc

Readme

@generazioneai/crypto

Cifratura a busta dei segreti salvati nei database della piattaforma (ADR-012, spec «Fondamenta enterprise» §4, tappa K1). Un solo formato, gx2, con dati autenticati (AAD) obbligatori; DEK per servizio e azienda, cifrate da una KEK che non esce dal KMS; lettura dei formati di oggi finché la migrazione (K2-K5) non li ha riscritti.

Installazione (npmjs.org, pubblico dalla 0.2.0 su npm; prima solo su GitHub Packages come @generazioneai-srl/crypto):

npm i @generazioneai/crypto

Formato

gx2.<dekId>.<iv>.<cifrato+tag>
  • AES-256-GCM, IV casuale di 12 byte, tag di 16 byte in coda al cifrato; iv e cifrato+tag in base64url senza =.
  • dekId: id della riga del registro encryption_keys del servizio.
  • AAD, costruita dalla libreria: i campi gx2, dekId, servizio, tabella, colonna, id della riga, azienda (o null per la piattaforma), ognuno [1 byte: 0 null, 1 testo][lunghezza u32 big-endian][UTF-8]. Un valore copiato su un'altra riga, colonna, tabella, azienda o servizio non si decifra. Un contesto incompleto (anche solo juridicalId dimenticato) è un errore: per i dati della piattaforma si scrive juridicalId: null.
  • Vettori di prova in vettori/gx2.json (deterministici) e vettori/legacy.json (formati vecchi, generati con il codice vero dei servizi): li usa anche il secretbox v3 di Go (Sentinel).

Uso

import { EnvelopeCrypto, InfisicalKmsKekProvider, LocalKekProvider, createPrismaDekStore, notifyDataKeyV1, parseKey32 } from '@generazioneai/crypto';

const kek = process.env.KMS_KEY_ID
  ? new InfisicalKmsKekProvider({
      siteUrl: process.env.INFISICAL_SITE_URL!,   // nel cluster: http://infisical-infisical-standalone-infisical.infisical.svc.cluster.local:8080
      keyId: process.env.KMS_KEY_ID,
      clientId: process.env.KMS_CLIENT_ID!,
      clientSecret: process.env.KMS_CLIENT_SECRET!,
    })
  : new LocalKekProvider({ keys: parseKey32('DEV_KEK', process.env.DEV_KEK)! }); // sviluppo e test; rifiutato in produzione

const box = new EnvelopeCrypto({
  service: 'notify',
  kek,
  store: createPrismaDekStore(prisma.encryptionKey),           // dentro runUnscoped('crypto-dek') con authz/RLS attive
  legacy: [notifyDataKeyV1(parseKey32('NOTIFY_DATA_KEY', process.env.NOTIFY_DATA_KEY)!)], // KEK di transizione
});

const ctx = { table: 'messages', column: 'payloadEnc', rowId: message.id, juridicalId: message.juridicalId };
const enc = await box.encrypt(ctx, JSON.stringify(payload));        // sempre gx2
const json = await box.decryptString(ctx, stored, { aad: message.id }); // gx2, o il blob v1 di oggi con la sua AAD
if (needsRewrite(stored)) { /* il job di migrazione lo riscriverà */ }

KEK e KMS

KekProvider è l'interfaccia (wrap/unwrap della DEK, kekId, versione):

  • InfisicalKmsKekProvider: KMS dell'Infisical self-hosted (login della machine identity con Universal Auth, token in cache; POST /api/v1/kms/keys/<keyId>/encrypt|decrypt). Il KMS non accetta AAD: l'involucro porta l'impronta della riga del registro e la controlla alla decifratura.
  • Login della machine identity (0.4.0): auth: 'universal' (default, clientId + clientSecret) o auth: 'kubernetes' (identityId + il token proiettato del service account del pod, riletto a ogni login da serviceAccountTokenPath, default /var/run/secrets/infisical-kms/token; Infisical lo verifica con la TokenReview dell'API server). Con kubernetes nel Secret del servizio non c'è nessuna credenziale del KMS.
  • LocalKekProvider: chiave da env, per sviluppo e test (o KEK di transizione con allowInProduction).

Più KEK e rewrap (0.2.0)

Promessa: si cambia provider di KMS, o si torna a una chiave locale, senza toccare un valore cifrato.

  • KekRing(corrente, [altre]): cifra le DEK nuove con la corrente, apre ogni DEK con la KEK scritta nella sua riga (kekId). È il kek da passare a EnvelopeCrypto durante un cambio.
  • rewrapDeks({ service, store, from, to, apply, force }): ricifra tutte le DEK del registro sotto to. A secco senza apply; si può rilanciare (quelle già sotto to alla sua versione si saltano); rilegge il nuovo involucro prima di scrivere; scrittura condizionata (replaceWrapped: una riga cambiata nel frattempo si lascia e si conta in changed). force ricifra anche quelle già sotto to: dopo una rotazione della chiave dentro Infisical. Stessa semantica di secretbox.Rewrap (Go). Le DEK non cambiano, quindi gx2.<dekId>.… resta valido.
  • kekRingFromEnv({ prefix: 'NOTIFY', production }): le KEK di un servizio dalle variabili, uguali dappertutto: INFISICAL_SITE_URL, KMS_KEY_ID, KMS_CLIENT_ID, KMS_CLIENT_SECRET (KMS_ORGANIZATION_SLUG facoltativa) per il KMS; con KMS_AUTH=kubernetes (0.4.0) al posto di client id e secret KMS_IDENTITY_ID e, facoltativa, KMS_K8S_TOKEN_PATH; <P>_KEK_LOCAL_KEYS (base64 di 32 byte, versioni separate da virgole: la posizione è la versione), <P>_KEK_LOCAL_NAME, <P>_KEK_LOCAL_IN_PRODUCTION; <P>_KEK = infisical | local sceglie quale cifra (di serie il KMS, se c'è).
  • createPrismaDekStore implementa anche listAll e replaceWrapped (servono findMany e updateMany del delegate).
  • KekProvider.kekVersion (facoltativo): la versione con cui wrap cifra; LocalKekProvider la più alta, Infisical 1.

Esempio, dal KMS di nuovo a una chiave locale: NOTIFY_KEK=local con NOTIFY_KEK_LOCAL_KEYS e il KMS ancora configurato, poi rewrapDeks (a secco, poi apply), poi via le variabili KMS_*.

Rotazione di una azienda (0.3.0)

Sospetto di compromissione della DEK di un'azienda (decisione 28): box.rotateTenant(juridicalId, { registry, db, batchSize }) ritira la DEK attiva, ne crea una nuova e ricifra a lotti i valori di quell'azienda. I valori restano gli stessi in chiaro; encrypt, decrypt e rewrapDeks non cambiano.

import { defineEncryptedFields, createSqlFieldsDb } from '@generazioneai/crypto';

// una volta, accanto ai segreti del servizio
export const ENCRYPTED_FIELDS = defineEncryptedFields('notify', [
  { table: 'profile_channels', column: 'secretsEnc', idColumn: 'id', tenantColumn: 'juridicalId' },
]);

// nel comando di rotazione (gateway: Pannello «Chiavi», con conferma PRODUZIONE; i dati della piattaforma non ruotano qui)
const report = await runAsTenantSystem(juridicalId, 'crypto-rotate-tenant', () =>
  box.rotateTenant(juridicalId, { registry: ENCRYPTED_FIELDS, db: createSqlFieldsDb(prisma), batchSize: 200 }),
);
  • idColumn è il rowId dell'AAD e tenantColumn il juridicalId: devono essere quelli con cui il servizio cifra. createSqlFieldsDb(client, { schema? }) usa $queryRawUnsafe/$executeRawUnsafe con identificatori fra virgolette e valori come parametri; si può passare un proprio EncryptedFieldsDb (readBatch, replaceValue). Con l'authz o la RLS attive il giro va in runAsTenantSystem (authz 0.7): vede solo quell'azienda.
  • Un valore si ricifra se non sta sotto la DEK attiva; la scrittura è condizionata (replaceValue vuole ancora il valore letto): una riga cambiata da altri si conta in changed e si lascia. La DEK vecchia resta con retiredAt e continua ad aprire i valori finché remaining non è 0; poi si può cancellare dal registro.
  • dryRun: true non scrive niente e non ritira (dice quante righe sono sotto una DEK diversa dall'attiva). resume: true non ritira la DEK attiva e ricifra solo i ritardatari: serve dopo un giro interrotto e, passati 15 minuti, per i valori scritti da repliche con la cache della DEK vecchia.
  • Il registro delle DEK deve saper ritirare (retireActive): InMemoryDekStore e createPrismaDekStore (serve updateMany).
  • I valori non gx2 (formati vecchi) si saltano (skippedLegacy): li riscrive il job di migrazione del servizio.
  • Il rapporto (RotateTenantReport) non contiene mai valori né chiavi; failed ha una riga per riga con tabella, colonna, id e motivo.

KEK di emergenza fuori linea (0.5.0)

Ogni DEK (e ogni chiave HMAC del registro, purpose = 'hmac:*') ha anche un secondo involucro, sigillato con una chiave pubblica X25519 la cui privata non sta né nel cluster né in Infisical: se Infisical si perde del tutto, il dump di encryption_keys e la privata restituiscono tutte le DEK.

const { ring } = kekRingFromEnv({ prefix: 'NOTIFY', production: true, allowedEmergencyKekIds: ['emergency:…'] });
// NOTIFY_KEK_EMERGENCY=on + KEK_EMERGENCY_PUBLIC_KEY=gxepub1:… → ogni DEK nuova ha emergencyWrapped/emergencyKekId
await escrowDeks({ service: 'notify', store, kek: ring!, emergency: ring!.emergency!, apply: true }); // le DEK di prima
  • Colonne del registro (facoltative, migrazione additiva prima di on): emergencyWrapped Bytes?, emergencyKekId String?.
  • EmergencyKek sigilla e basta: nessun servizio può aprire un involucro di emergenza. EmergencyRecoveryKey (la privata) si usa solo fuori linea ed è rifiutata dentro un pod.
  • Recupero: gx2-emergenza controlla|apri|riavvolgi|pubblica --chiave <file> --dump <csv> --servizio <svc>; prova completa su un Postgres usa e getta con bash scripts/prova-recupero-pg.sh.
  • Formato gxe1, file della privata gxe-priv-1, procedura e prova annuale: vault, «Sicurezza/Runbook - KEK di emergenza».

Segnali d'uso del KMS per Sentinel (0.6.0)

0.9 (decisione del 2026-11-10, via gli interruttori): accesi se la configurazione c'è (KMS_SIGNALS_SERVICE o DENY_SIGNALS_SERVICE valido e un NATS), altrimenti spenti con una riga nel log; KMS_SIGNALS=off li spegne solo in sviluppo locale e nei test. Accesi, ogni EnvelopeCrypto conta, per DEK, le decifrature riuscite (decrypts), quelle col tag sbagliato (decryptErrors), i valori di un altro ambito (wrongScope), le aperture di DEK col KMS (unwraps, unwrapErrors) e le DEK nuove (created), e una volta per finestra (60 s) pubblica un messaggio su sec.kms.<servizio>: { v:1, id, service, subject, from, to, windowMs, totals, droppedEntries, entries:[{ dek, scope, tenant?, purpose, …contatori }] }. Solo id di DEK e di aziende (UUID): niente valori, testi, righe o persone. La registrazione è una somma in una mappa (spenta: un confronto); il publish è core, su una connessione NATS sua (@nats-io/transport-node o nats, se installati), e con NATS giù si butta e si conta. Tetti: KMS_SIGNALS_MAX_KEYS (2000 DEK in mappa), KMS_SIGNALS_MAX_ENTRIES (500 voci per messaggio, le più attive).

| Variabile | Di serie | |---|---| | KMS_SIGNALS | acceso se configurato (off solo in sviluppo locale) | | KMS_SIGNALS_SERVICE | DENY_SIGNALS_SERVICE: l'utente NATS, che deve poter pubblicare su sec.kms.<servizio> | | KMS_SIGNALS_WINDOW_MS, KMS_SIGNALS_MAX_KEYS, KMS_SIGNALS_MAX_ENTRIES | 60000, 2000, 500 | | KMS_SIGNALS_NATS_URL | NATS_URL (con NATS_NKEY_SEED) |

Firma (0.8.0; 0.9: sempre, EVENTS_SIGNATURE=off solo in sviluppo locale): ogni messaggio porta Gen-Sig (token di @generazioneai/authz ≥ 0.7.3, chiave della firma interna del servizio, vedi «Gen-Sig dei segnali» nel README di authz); la verifica la fa Sentinel.

new EnvelopeCrypto({ …, usage }) accetta un KmsUsageSink proprio; usage: null non registra mai. Sentinel ne ricava la regola «decifrature anomale» rispetto alla norma del servizio.

Metriche OpenTelemetry del KMS (0.8.0)

Sempre accese ma senza costo se manca l'API: @opentelemetry/api è un peer facoltativo (nessuna dipendenza nuova). Con l'API installata e un SDK registrato (@generazioneai/telemetry) ogni EnvelopeCrypto scrive:

| Metrica | Attributi | |---|---| | kms_operations_total | op (wrap = DEK nuova, unwrap = DEK aperta), outcome (ok, unavailable = KMS non raggiungibile, error), purpose | | kms_operation_duration_seconds | op, outcome (istogramma, comprese le riprove del provider) | | kms_decrypt_total | outcome (ok, error = tag che non torna, wrong_scope = DEK di un'altra azienda o scopo), purpose | | kms_stale_served_total | purpose (DEK scaduta usata col KMS giù, staleIfKmsDownMs) |

Il servizio lo dice la risorsa (service_name). Mai id di DEK o aziende: quelli vanno solo nei segnali per Sentinel qui sopra. setKmsMeter(meter) per i test (null = nessuna metrica). Regola kms-errori di vmalert nel Runbook - Osservabilità.

Indice cieco dei dati personali (0.7.0)

La ricerca per uguaglianza su un dato personale cifrato (codice fiscale, username, numero di documento) si fa su un indice cieco:

import { blindIndex, deriveIndexKey, fieldIndexInfo, PII_INDEX_PURPOSE } from '@generazioneai/crypto';
// dataKey: 32 byte del registro delle DEK (purpose = PII_INDEX_PURPOSE), aperti col KMS una volta per processo.
const k = deriveIndexKey(dataKey, { salt: 'genai-hr', info: fieldIndexInfo('Candidate', 'fiscalCode') });
blindIndex(' rss mra 80a01 h501u ', k); // 'b1.' + HMAC-SHA256 di 'RSSMRA80A01H501U' (base64url)

normIdentifier (NFKC, senza spazi anche Unicode, maiuscolo; gli omocodici non si riconducono), sha256Index (s1., impronta senza chiave solo per segreti casuali, es. token di 256 bit), isPersonFiscalCode. Sale e info sono quelli del servizio: ID genai-id / pii-identifier (una sottochiave per username e CF), HR genai-hr / pii-index:<Modello>.<campo> (una per campo). Vettori in vettori/blind-index.json, presi dalle implementazioni dei servizi: chi passa alla libreria ritrova gli indici già scritti.

DEK, cache e guasti

  • Una DEK per servizio, azienda e scopo (secrets), più una platform; creata alla prima cifratura, una sola anche con molte richieste insieme o due repliche (conflitto → si rilegge la riga attiva). Il registro vuole l'indice unico parziale scritto in src/dek-store.ts.
  • DEK in chiaro in memoria 15 minuti, al più 1.000; mai nei log. KMS giù: le DEK in cache funzionano (e una scaduta da meno di staleIfKmsDownMs, 4 ore, si usa con un avviso); una DEK nuova fallisce con DekUnavailableError. La prontezza dei pod non deve dipendere dal KMS. stats() per le metriche.

Formati vecchi letti

| formato | servizio, codice di oggi | chiave | AAD da passare in hint | |---|---|---|---| | id-fe1 (fe1.idk1.…) | ID shared/crypto/field-crypto.ts (TOTP, IBAN) | ID_DATA_KEY (HKDF field:<scopo>) | purpose + aad = <scopo>\|<contesto> | | id-sso-v1 (v1.…) | ID shared/sso/sso-crypto.ts | ID_DATA_KEY (HKDF sso-secret) | <configId>\|<campo> | | notify-v1 (blob [1][iv][tag][…], byte o base64) | Notify utilities/crypto/aes-gcm.ts | NOTIFY_DATA_KEY | quella di oggi (id del messaggio, notify-profile-secret:v1\|…) | | sign-v1 | Sign utilities/crypto/aes-gcm.ts | SIGN_DATA_KEY | account e provider | | ops-v1 (v1.…) | Ops integrations/core/secrets.ts | OPS_SECRETS_KEY | nessuna | | train-xapi-v1 (v1:…) | Train xapi/utils/crypto.util.ts | XAPI_SECRETS_KEY | nessuna |

Non ancora: le segnalazioni di HR (WB_ENCRYPTION_KEY, HKDF per azienda, testi e file WB1) e Sentinel (Go), nelle tappe K3 e K4.