Abbiamo visto come una libreria può essere utilizzata per attivare i messaggi push, ma cosa cosa fanno esattamente queste biblioteche?
Sta effettuando richieste di rete, garantendo al contempo che siano nel formato corretto. La specifica che definisce questa richiesta di rete Web Push Protocol.
Questa sezione illustra come il server può identificarsi con l'applicazione le chiavi server e come vengono inviati il payload criptato e i dati associati.
Non si tratta di un bel lato del web push e non ho esperienza in fatto di crittografia, ma diamo un'occhiata ogni parte, poiché è utile sapere cosa fanno dietro le quinte.
Chiavi server delle applicazioni
Quando sottoscriviamo l'iscrizione di un utente, trasmettiamo un applicationServerKey
. Questa chiave è
passati al servizio push e utilizzati per verificare che l'applicazione che ha sottoscritto l'abbonamento
l'utente è anche l'applicazione che attiva i messaggi push.
Quando attiviamo un messaggio push, viene inviato un insieme di intestazioni che consentire al servizio push di autenticare l'applicazione. (Questo viene definito in base alle specifiche VAPID).
Cosa significa tutto questo e cosa accade esattamente? Bene, questi sono i passaggi seguiti autenticazione server delle applicazioni:
- Il server delle applicazioni firma alcune informazioni JSON con la tua chiave applicazione privata.
- Queste informazioni firmate vengono inviate al servizio push come intestazione in una richiesta POST.
- Il servizio push utilizza la chiave pubblica archiviata che ha ricevuto
pushManager.subscribe()
per verificare che le informazioni ricevute siano firmate da la chiave privata relativa alla chiave pubblica. Ricorda: la chiave pubblica èapplicationServerKey
passato alla chiamata di sottoscrizione. - Se le informazioni firmate sono valide, il servizio push invia il push all'utente.
Di seguito è riportato un esempio di questo flusso di informazioni. (nota la legenda in basso a sinistra per indicare pubbliche e private).
Le "informazioni firmate" aggiunto a un'intestazione nella richiesta è un token web JSON.
Token web JSON
Un token web JSON (o JWT in breve) è un modo un messaggio a terze parti in modo che il destinatario possa convalidare chi l'ha inviato.
Quando una terza parte riceve un messaggio, deve recuperare i mittenti chiave pubblica e utilizzarla per convalidare la firma del JWT. Se valida, il JWT deve essere stato firmato con il valore quindi deve provenire dal mittente previsto.
Su https://jwt.io/ sono disponibili varie librerie che eseguire la firma al posto tuo e ti consiglio di farlo dove è in grado di eseguire. Per completezza, vediamo come creare manualmente un JWT firmato.
Web push e JWT firmati
Un JWT firmato è solo una stringa, anche se può essere considerato come un insieme di tre stringhe per punti.
La prima e la seconda stringa (informazioni JWT e dati JWT) sono parti JSON che è stato codificato in base64, il che significa che è leggibile pubblicamente.
La prima stringa contiene informazioni sul JWT stesso, che indica quale algoritmo è stato utilizzato per creare la firma.
Le informazioni JWT per il push web devono contenere le seguenti informazioni:
{
"typ": "JWT",
"alg": "ES256"
}
La seconda stringa è costituita dai dati JWT. In questo modo vengono fornite informazioni sul mittente del JWT, che è destinato e per quanto tempo è valido.
Per il push web, i dati avranno il seguente formato:
{
"aud": "https://some-push-service.org",
"exp": "1469618703",
"sub": "mailto:example@web-push-book.org"
}
Il valore aud
è il "pubblico", ovvero a chi è destinato il JWT. Per il web, esegui il push
è il servizio push, pertanto lo impostiamo sull'origine del push
Google Cloud.
Il valore exp
è la scadenza del JWT, impedendo così ai snooper di essere
in grado di riutilizzare un JWT se lo intercettano. La scadenza è un timestamp in
secondi e non deve durare più di 24 ore.
In Node.js la scadenza viene impostata utilizzando:
Math.floor(Date.now() / 1000) + 12 * 60 * 60;
Sono necessarie 12 ore anziché 24 ore per evitare eventuali problemi con le differenze di orologio tra l'applicazione di invio e il servizio push.
Infine, il valore sub
deve essere un URL o un indirizzo email mailto
.
In questo modo, se un servizio push ha bisogno di contattare il mittente, può trovare
le informazioni di contatto del JWT. (Ecco perché la libreria web-push aveva bisogno di
).
Proprio come le informazioni JWT, i dati JWT sono codificati come base sicura per gli URL64 stringa.
La terza stringa, la firma, è il risultato dell'utilizzo delle prime due stringhe (JWT Info e JWT Data), unendoli con un punto, che analizzeremo chiamare il "token non firmato" e firmarlo.
Il processo di firma richiede la crittografia del "token non firmato" utilizzando ES256. In base al JWT specifiche, ES256 è l'abbreviazione di "ECDSA usando la curva P-256" l'algoritmo hash SHA-256". Utilizzando la crittografia web, puoi creare la firma in questo modo:
// Utility function for UTF-8 encoding a string to an ArrayBuffer.
const utf8Encoder = new TextEncoder('utf-8');
// The unsigned token is the concatenation of the URL-safe base64 encoded
// header and body.
const unsignedToken = .....;
// Sign the |unsignedToken| using ES256 (SHA-256 over ECDSA).
const key = {
kty: 'EC',
crv: 'P-256',
x: window.uint8ArrayToBase64Url(
applicationServerKeys.publicKey.subarray(1, 33)),
y: window.uint8ArrayToBase64Url(
applicationServerKeys.publicKey.subarray(33, 65)),
d: window.uint8ArrayToBase64Url(applicationServerKeys.privateKey),
};
// Sign the |unsignedToken| with the server's private key to generate
// the signature.
return crypto.subtle.importKey('jwk', key, {
name: 'ECDSA', namedCurve: 'P-256',
}, true, ['sign'])
.then((key) => {
return crypto.subtle.sign({
name: 'ECDSA',
hash: {
name: 'SHA-256',
},
}, key, utf8Encoder.encode(unsignedToken));
})
.then((signature) => {
console.log('Signature: ', signature);
});
Un servizio push può convalidare un JWT utilizzando la chiave server delle applicazioni pubblica per decriptare la firma e assicurarsi che la stringa decriptata sia la stessa come "token non firmato" (ovvero le prime due stringhe nel JWT).
Il JWT firmato (ovvero tutte e tre le stringhe unite da punti) viene inviato al web
servizio push come intestazione Authorization
con anteposto WebPush
, in questo modo:
Authorization: 'WebPush [JWT Info].[JWT Data].[Signature]';
Il Web Push Protocol indica inoltre che la chiave server dell'applicazione pubblica deve essere
inviata nell'intestazione Crypto-Key
come stringa codificata in base64 sicura per l'URL con
p256ecdsa=
anteposta.
Crypto-Key: p256ecdsa=[URL Safe Base64 Public Application Server Key]
La crittografia del payload
Ora vediamo come possiamo inviare un payload con un messaggio push in modo che, quando la nostra applicazione web riceve un messaggio push, può accedere ai dati che riceve.
Chi ha utilizzato altri servizi push spesso pone una domanda comune: perché il web esegue il push il payload debba essere criptato? Con le app native, i messaggi push possono inviare dati come testo normale.
Uno dei vantaggi del push web è che, poiché tutti i servizi push utilizzano la stessa API (il protocollo web push), gli sviluppatori non devono preoccuparsi servizio push. Possiamo presentare la richiesta nel formato corretto e prevediamo messaggio push da inviare. Lo svantaggio è che gli sviluppatori potrebbero è verosimilmente per inviare messaggi a un servizio push non attendibile. Di crittografando il payload, un servizio push non può leggere i dati inviati. Solo il browser può decriptare le informazioni. In questo modo viene protetto e i dati di Google Cloud.
La crittografia del payload è definita nel documento Message Encryption (Crittografia dei messaggi) del modello.
Prima di dare un'occhiata ai passaggi specifici per criptare un payload dei messaggi push, parleremo di alcune tecniche che verranno utilizzate durante e il processo di sviluppo. (L'enorme punta del cappello a Mat Scales per il suo eccellente articolo sulla encryption.)
ECDH e HKDF
Sia ECDH che HKDF vengono utilizzati in tutto il processo di crittografia e offrono vantaggi al allo scopo di criptare le informazioni.
ECDH: scambio di chiavi Diffie-Hellman con curva ellittica
Immagina di avere due persone che desiderano condividere informazioni, Alice e Bob. Sia Alice che Bob hanno le proprie chiavi pubbliche e private. Alice e Bob condividono tra loro le chiavi pubbliche.
L'utile proprietà delle chiavi generate con ECDH è che Alice può utilizzare e la chiave pubblica di Roberto per creare il valore del secret "X". Bob può fare nello stesso modo, prendendo la sua chiave privata e la chiave pubblica di Alice creano singolarmente lo stesso valore "X". In questo modo, "X" un secret condiviso e Alice e Bob hanno dovuto solo condividere la loro chiave pubblica. Ora Bob e Alice puoi usare "X" per criptare e decriptare i messaggi tra di loro.
ECDH, per quanto mi risulta, definisce le proprietà delle curve che consentono questa "caratteristica" di creare una "X" segreta condivisa.
Questa è una spiegazione generale di ECDH. Se vuoi saperne di più, ti consiglio di guardare questo video.
In termini di codice, la maggior parte dei linguaggi / piattaforme include librerie per rendere è facile generare queste chiavi.
Nel nodo, eseguiremmo queste operazioni:
const keyCurve = crypto.createECDH('prime256v1');
keyCurve.generateKeys();
const publicKey = keyCurve.getPublicKey();
const privateKey = keyCurve.getPrivateKey();
HKDF: funzione di derivazione della chiave basata su HMAC
Wikipedia contiene una breve descrizione di HKDF:
HKDF è una funzione di derivazione della chiave basata su HMAC che trasforma qualsiasi chiave debole in materiale delle chiavi con un'efficacia crittografica. Può essere usato, ad esempio, esempio, per convertire Diffie Hellman ha scambiato i segreti condivisi in materiale delle chiavi adatte all'uso per la crittografia, il controllo dell'integrità o l'autenticazione.
In sostanza, HKDF prenderà gli input non particolarmente sicuri e li renderà più sicuri.
La specifica che definisce questa crittografia richiede l'utilizzo di SHA-256 come algoritmo hash e le chiavi risultanti per HKDF nel push web non dovrebbero essere più lunghe di 256 bit (32 byte).
Nel nodo potrebbe essere implementato in questo modo:
// Simplified HKDF, returning keys up to 32 bytes long
function hkdf(salt, ikm, info, length) {
// Extract
const keyHmac = crypto.createHmac('sha256', salt);
keyHmac.update(ikm);
const key = keyHmac.digest();
// Expand
const infoHmac = crypto.createHmac('sha256', key);
infoHmac.update(info);
// A one byte long buffer containing only 0x01
const ONE_BUFFER = new Buffer(1).fill(1);
infoHmac.update(ONE_BUFFER);
return infoHmac.digest().slice(0, length);
}
Consiglio per l'articolo di Mat Scale per questo esempio di codice.
Questa descrizione copre genericamente ECDH e HKDF.
ECDH un modo sicuro per condividere le chiavi pubbliche e generare un secret condiviso. L'HKDF è un modo per materiale non sicuro e renderlo sicuro.
Verrà utilizzato durante la crittografia del nostro payload. Ora vediamo cosa prendiamo e come viene criptato.
Input
Per inviare un messaggio push a un utente con un payload, dobbiamo utilizzare tre input:
- Il payload stesso.
- Il secret
auth
diPushSubscription
. - La chiave
p256dh
diPushSubscription
.
Abbiamo notato che i valori auth
e p256dh
vengono recuperati da un PushSubscription
, ma per un
un breve promemoria, dato un abbonamento, avremmo bisogno di questi valori:
subscription.toJSON().keys.auth;
subscription.toJSON().keys.p256dh;
subscription.getKey('auth');
subscription.getKey('p256dh');
Il valore auth
deve essere considerato come un secret e non deve essere condiviso all'esterno dell'applicazione.
La chiave p256dh
è una chiave pubblica, a volte indicata come chiave pubblica del client. Qui
Ci riferiremo a p256dh
come chiave pubblica dell'abbonamento. Viene generata la chiave pubblica dell'abbonamento
dal browser. Il browser manterrà il segreto della chiave privata e la utilizzerà per decriptare
per il payload.
Questi tre valori, auth
, p256dh
e payload
sono necessari come input e il risultato
di crittografia è il payload criptato, un valore di sale e una chiave pubblica
la crittografia dei dati.
Saldo
Il sale deve essere di 16 byte di dati casuali. In NodeJS, per creare un salt, eseguiremo la seguente procedura:
const salt = crypto.randomBytes(16);
Chiavi pubbliche / private
Le chiavi pubbliche e private devono essere generate utilizzando una curva ellittica P-256, che faremmo in Node, in questo modo:
const localKeysCurve = crypto.createECDH('prime256v1');
localKeysCurve.generateKeys();
const localPublicKey = localKeysCurve.getPublicKey();
const localPrivateKey = localKeysCurve.getPrivateKey();
Ci riferiremo a queste chiavi come "chiavi locali". Vengono utilizzati solo per la crittografia e sono niente a che fare con le chiavi dei server delle applicazioni.
Con il payload, il segreto di autenticazione e la chiave pubblica di abbonamento come input e con una sale e un set di chiavi locali, siamo pronti a eseguire un po' di crittografia.
Secret condiviso
Il primo passaggio consiste nel creare un secret condiviso utilizzando la chiave pubblica di abbonamento e il nostro nuovo chiave privata (ricordi la spiegazione relativa a ECDH presentata ad Alice e Bob? è semplicissimo).
const sharedSecret = localKeysCurve.computeSecret(
subscription.keys.p256dh,
'base64',
);
che verrà utilizzata nel passaggio successivo per calcolare la pseudo chiave casuale (PRK).
Chiave pseudocasuale
La chiave pseudo casuale (PRK) è la combinazione dell'autenticazione della sottoscrizione push e il secret condiviso che abbiamo appena creato.
const authEncBuff = new Buffer('Content-Encoding: auth\0', 'utf8');
const prk = hkdf(subscription.keys.auth, sharedSecret, authEncBuff, 32);
Forse ti starai chiedendo a cosa serve la stringa Content-Encoding: auth\0
.
In breve, non ha uno scopo chiaro, anche se i browser potrebbero
decriptare un messaggio in arrivo e cercare la codifica dei contenuti prevista.
\0
aggiunge un byte con valore 0 alla fine del buffer. Questo è
atteso dai browser che decriptano il messaggio, che si aspettano così tanti byte
per la codifica dei contenuti, seguito da un byte con valore 0 e
ai dati criptati.
La nostra pseudo chiave casuale esegue semplicemente l'autenticazione, il segreto condiviso e un'informazione di codifica tramite HKDF (ovvero la maggiore sicurezza crittografica).
Contesto
Il "contesto" è un insieme di byte utilizzato per calcolare due valori in un secondo momento del browser. Si tratta essenzialmente di un array di byte contenente la chiave pubblica dell'abbonamento e chiave pubblica locale.
const keyLabel = new Buffer('P-256\0', 'utf8');
// Convert subscription public key into a buffer.
const subscriptionPubKey = new Buffer(subscription.keys.p256dh, 'base64');
const subscriptionPubKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = subscriptionPubKey.length;
const localPublicKeyLength = new Uint8Array(2);
subscriptionPubKeyLength[0] = 0;
subscriptionPubKeyLength[1] = localPublicKey.length;
const contextBuffer = Buffer.concat([
keyLabel,
subscriptionPubKeyLength.buffer,
subscriptionPubKey,
localPublicKeyLength.buffer,
localPublicKey,
]);
Il buffer di contesto finale è un'etichetta, il numero di byte nella chiave pubblica della sottoscrizione, seguito dalla chiave stessa, quindi dal numero di byte della chiave pubblica locale, seguito dalla chiave per trovare le regole.
Con questo valore di contesto possiamo utilizzarlo nella creazione di un nonce e di una chiave di crittografia dei contenuti (CEK).
Chiave di crittografia dei contenuti e nonce
nonce è un valore che impedisce la riproduzione perché dovrebbe essere usato una sola volta.
La chiave di crittografia dei contenuti (CEK) è la chiave che verrà utilizzata per criptare il payload.
Per prima cosa dobbiamo creare i byte di dati per il nonce e CEK, che sono semplicemente stringa di codifica seguita dal buffer di contesto che abbiamo appena calcolato:
const nonceEncBuffer = new Buffer('Content-Encoding: nonce\0', 'utf8');
const nonceInfo = Buffer.concat([nonceEncBuffer, contextBuffer]);
const cekEncBuffer = new Buffer('Content-Encoding: aesgcm\0');
const cekInfo = Buffer.concat([cekEncBuffer, contextBuffer]);
Queste informazioni vengono gestite tramite HKDF che combina salt e PRK con nonceInfo e cekInfo:
// The nonce should be 12 bytes long
const nonce = hkdf(salt, prk, nonceInfo, 12);
// The CEK should be 16 bytes long
const contentEncryptionKey = hkdf(salt, prk, cekInfo, 16);
Questo ci fornisce il nostro nonce e la chiave di crittografia dei contenuti.
Eseguire la crittografia
Ora che abbiamo la nostra chiave di crittografia dei contenuti, possiamo criptare il payload.
Creiamo una crittografia AES128 utilizzando la chiave di crittografia dei contenuti come chiave e il nonce è un vettore di inizializzazione.
In Node questo avviene in questo modo:
const cipher = crypto.createCipheriv(
'id-aes128-GCM',
contentEncryptionKey,
nonce,
);
Prima di criptare il nostro payload, dobbiamo definire la quantità di spaziatura interna che vogliamo da aggiungere alla parte anteriore del payload. Il motivo per cui dovremmo aggiungere spaziatura interna è che previene il rischio che le intercettazioni siano in grado di determinare "tipi" di messaggi in base alle dimensioni del payload.
Devi aggiungere due byte di spaziatura interna per indicare la lunghezza dell'eventuale spaziatura interna aggiuntiva.
Ad esempio, se non hai aggiunto spaziatura interna, avrai due byte con valore 0, ovvero non esiste alcuna spaziatura interna, dopo questi due byte leggerai il payload. Se hai aggiunto 5 byte di spaziatura interna, i primi due byte avranno un valore di 5, quindi il consumer leggerà altri cinque byte e inizierà a leggere il payload.
const padding = new Buffer(2 + paddingLength);
// The buffer must be only zeros, except the length
padding.fill(0);
padding.writeUInt16BE(paddingLength, 0);
Quindi eseguiamo la spaziatura interna e il payload attraverso questa crittografia.
const result = cipher.update(Buffer.concat(padding, payload));
cipher.final();
// Append the auth tag to the result -
// https://nodejs.org/api/crypto.html#crypto_cipher_getauthtag
const encryptedPayload = Buffer.concat([result, cipher.getAuthTag()]);
Ora abbiamo il nostro payload criptato. Benissimo!
Tutto ciò che rimane da determinare è determinare come il payload viene inviato al servizio push.
Intestazioni di payload crittografati corpo
Per inviare questo payload criptato al servizio push, dobbiamo definire a diverse intestazioni nella nostra richiesta POST.
Intestazione crittografia
La sezione "Crittografia" deve contenere il salt utilizzato per criptare il payload.
Il sale da 16 byte deve essere un URL base64 codificato in modo sicuro e aggiunto all'intestazione Crittografia, in questo modo:
Encryption: salt=[URL Safe Base64 Encoded Salt]
Intestazione Crypto-Key
Abbiamo notato che l'intestazione Crypto-Key
è utilizzata sotto la colonna "Application Server Keys" (Chiavi server applicazione)
contenente la chiave pubblica del server delle applicazioni.
Questa intestazione viene utilizzata anche per condividere la chiave pubblica locale utilizzata per criptare per il payload.
L'intestazione risultante ha il seguente aspetto:
Crypto-Key: dh=[URL Safe Base64 Encoded Local Public Key String]; p256ecdsa=[URL Safe Base64 Encoded Public Application Server Key]
Tipo di contenuti, lunghezza e codifica intestazioni
L'intestazione Content-Length
indica il numero di byte nell'elenco
per il payload. 'Content-Type' e "Content-Encoding" sono valori fissi.
come mostrato di seguito.
Content-Length: [Number of Bytes in Encrypted Payload]
Content-Type: 'application/octet-stream'
Content-Encoding: 'aesgcm'
Con queste intestazioni impostate, dobbiamo inviare il payload criptato
della nostra richiesta. Nota che Content-Type
è impostato su
application/octet-stream
. Questo perché il payload criptato deve essere
inviato come flusso di byte.
In NodeJS seguiamo questa procedura:
const pushRequest = https.request(httpsOptions, function(pushResponse) {
pushRequest.write(encryptedPayload);
pushRequest.end();
Altre intestazioni?
Abbiamo esaminato le intestazioni utilizzate per le chiavi JWT / Application Server (ovvero come identificare con il servizio push) e abbiamo esaminato le intestazioni utilizzate per inviare un messaggio per il payload.
Esistono altre intestazioni che utilizzano i servizi push per modificare il comportamento messaggi inviati. Alcune di queste intestazioni sono obbligatorie, mentre altre sono facoltative.
Intestazione TTL
Obbligatorio
TTL
(o time to live) è un numero intero che specifica il numero di secondi
pubblicare il messaggio push sul servizio push prima che sia
sono recapitate. Alla scadenza di TTL
, il messaggio verrà rimosso dal
dalla coda del servizio push e non verrà consegnato.
TTL: [Time to live in seconds]
Se imposti un valore TTL
pari a zero, il servizio push tenterà di consegnare
immediatamente, ma se il dispositivo non è raggiungibile, il messaggio
verrà eliminato immediatamente dalla coda del servizio push.
Tecnicamente un servizio push può ridurre il numero di TTL
di un messaggio push se
vuole. Puoi stabilirlo esaminando l'intestazione TTL
in
la risposta da un servizio push.
Argomento
Facoltativo
Gli argomenti sono stringhe che possono essere utilizzate per sostituire un messaggio in attesa con un nuovo messaggio se hanno nomi di argomenti corrispondenti.
Ciò è utile negli scenari in cui vengono inviati più messaggi durante un dispositivo è offline e vuoi che l'utente veda solo l'ultima quando il dispositivo è acceso.
Urgenza
Facoltativo
L'urgenza indica al servizio push quanto sia importante un messaggio per l'utente. Questo può essere utilizzato dal servizio push per preservare la durata della batteria del dispositivo di un utente solo svegliarti per ricevere messaggi importanti quando la batteria è in esaurimento.
Il valore dell'intestazione viene definito come mostrato di seguito. Il valore predefinito
è normal
.
Urgency: [very-low | low | normal | high]
Tutti insieme
Se hai altre domande su come funziona questa procedura, puoi sempre vedere come vengono attivate le librerie i messaggi push sull'organizzazione web-push-libs.
Una volta che il payload criptato e le intestazioni riportate sopra sono presenti, basta effettuare una richiesta POST
al endpoint
in un PushSubscription
.
Che cosa dobbiamo fare con la risposta a questa richiesta POST?
Risposta dal servizio push
Dopo aver inviato una richiesta a un servizio push, devi controllare il codice di stato della risposta, che ti indicherà se la richiesta è andata a buon fine o meno.
Codice di stato | Descrizione |
---|---|
201 | Creata. La richiesta di inviare un messaggio push è stata ricevuta e accettata. |
429 | Troppe richieste. il che significa che il tuo server delle applicazioni ha raggiunto una frequenza con un servizio push. Il servizio push deve includere un messaggio "Riprova dopo" per indicare quanto tempo deve passare prima che sia possibile effettuare un'altra richiesta. |
400 | Richiesta non valida. In genere questo significa che una delle intestazioni non è valida. o non è stato formattato correttamente. |
404 | Non trovato. Questo indica che l'abbonamento è scaduto e non possono essere utilizzate. In questo caso devi eliminare "PushSubscription" e attendere che il client si iscriva nuovamente all'utente. |
410 | Non più disponibile. L'abbonamento non è più valido e deve essere rimosso dal server delle applicazioni. Può essere riprodotto richiamando "unsubscribe()" su "PushSubscription". |
413 | Dimensioni del payload troppo grandi. Il payload di dimensione minima che un servizio push deve Le dimensioni supportate sono 4096 byte (o 4 kB). |
Passaggi successivi
- Panoramica delle notifiche push web
- Come funziona il push
- Iscrizione di un utente
- Esperienza utente con autorizzazione
- Invio di messaggi con le librerie push sul web
- Protocollo push web
- Gestione degli eventi push
- Visualizzazione di una notifica
- Comportamento delle notifiche
- Pattern di notifica comuni
- Domande frequenti sulle notifiche push
- Problemi comuni e segnalazione di bug