Graine AI
Enterprise

How Graine handles your keys

For security reviewers — the push and Play Integrity credentials your team gives Graine, and exactly how they are encrypted, kept write-only, scoped, validated, audited, monitored, rotated and revoked; the least-privilege setup for Apple and Google; and keyless access to Google.

With hosted ringing, Graine sends every ring to your app's users: a VoIP push on iPhone and a data message on Android, sent with your app's own push credentials, because only those can reach your app. To check that a request comes from your genuine Android app, Graine also decodes Play Integrity tokens with your Google Cloud credential. This page is for the people who review those credentials: exactly how Graine protects, scopes, audits, rotates and revokes them.

Graine's own push accounts are never used for your app. Graine's own apps send their notifications through push accounts of their own, including a third-party push service. None of those accounts ever sends to your app: every ring to your app's users is sent with the credentials you upload, and those credentials are used for your app and nothing else.

What is live, and what comes next

Items marked Coming after SDK 0.14.4 are built for the release that follows SDK 0.14.4 and are not available yet. Everything else on this page describes the platform as it runs today.

The credentials

CredentialWhat you uploadWhy Graine needs itWhat you see after saving
Apple push key (iPhone rings)The APNs authentication key (.p8), its Key ID, your Team ID, the app's bundle ID and the environmentA VoIP push that rings a phone through CallKit can only be sent with a key from the Apple team that owns the appBundle ID, status and when it was last checked, today. Key ID, Team ID, environment, who uploaded it and when, last used and delivery health: Coming after SDK 0.14.4
Firebase credential (Android rings)The FCM v1 service-account JSON of your Firebase project and the app's package name, or keyless access (Coming after SDK 0.14.4)A high-priority data message that rings a phone can only be sent through the Firebase project that issued its push tokenPackage name, status and when it was last checked, today. Project ID, service-account email, who uploaded it and when, last used and delivery health: Coming after SDK 0.14.4
Play Integrity credential (Android app checks)The service-account JSON of the Google Cloud project linked to your app in Play Console, one per package, or keyless access (Coming after SDK 0.14.4)Google decodes an integrity token only for the project linked to the appPackage name, status and the reason when it needs setup, today. Project ID and number, service-account email, last used and verification health: Coming after SDK 0.14.4

You add them in the dashboard: push credentials under Workspace settings → In-app calling (the same card is on each agent under Calls & scheduling), the Play Integrity credential under the agent's Embed & Widgets → Install → Your app and your users. iPhone app checks use Apple App Attest, which needs no key at all.

Encrypted in transit and at rest

In transit. The key file is read in your browser and sent once, over TLS, to Graine's API. Graine reaches Apple and Google over TLS: HTTP/2 to Apple's push service, HTTPS to Google.

At rest. Each credential is a separate entry in a managed secrets store in Mumbai, India, which protects it with envelope encryption under a cloud key-management service (KMS):

  • each credential has its own 256-bit AES data key, and a new one each time the credential is replaced;
  • that data key is stored only encrypted by a KMS key, and is decrypted by KMS when the credential is read;
  • every KMS request is cryptographically bound to the credential it is for, so a data key cannot be used for any other credential.

Graine's database holds only the entry's name, never the credential. A dedicated or on-premises deployment keeps credentials in that deployment's own encrypted store.

A KMS key reserved for these credentials, whose key policy admits only the services that use them, is provisioned per deployment and is not yet live on the shared platform. Ask us for the state of the environment you will run against; we give it in writing.

Write-only

Once saved, a credential is never shown again: not to you, not to Graine's staff, and not in any screen, API answer, log line or audit row. No support tool reads it. The dashboard shows only details that are not secret (the table above), and you can Replace or Remove a credential at any time.

Validated before it is saved

Graine tries every credential with Apple or Google before it keeps it, and shows that it works (Verified for a push credential, Working for a Play Integrity credential) or the exact reason it doesn't. Nobody's phone rings and no user's token is decoded.

CredentialThe checkWhat the answer tells apart
Apple push keyGraine signs a provider token with your key and sends a VoIP push on your app's topic (<bundle id>.voip) to a device token that belongs to nobody400 BadDeviceToken: Apple accepted the key and the topic and refused only the device, so the key is valid. 403 InvalidProviderToken: the key, Key ID or Team ID is wrong, or they don't belong together. TopicDisallowed or BadTopic: the key may not send for this app
Firebase credentialGraine takes a Google access token with your service account, then sends a validate_only message, which Google checks and never deliversA refused key, a project that does not exist, or a service account without permission to send for the project
Play Integrity credentialGraine takes a Google access token, then asks Google to decode a placeholder that is not a token400: the setup works. 403 or 404: the API is off or the project is not linked to the app, saved as Needs setup with what to do. A key Google refuses outright: not saved

A push credential that Apple or Google refuses is not stored, nor is a Play Integrity key that Google refuses outright; a working credential already on file stays as it was. When Apple or Google can't be reached, a push credential is not saved (try again in a minute), and a Play Integrity key is saved as Needs setup and checked again when it is next used. A saved push credential is checked again by itself before Graine ever marks it broken (see Health and alerts).

Check now, to run the same check on a saved credential at any time: Coming after SDK 0.14.4.

Scoped access

Who can change a credential. Admins, or a custom role with Manage integrations, can upload, replace and remove it. Every agent uses these credentials, so someone whose access is limited to some agents cannot change them. People with View integrations see the non-secret details and the status; nobody else sees the cards.

What can use it. Only the platform services that send rings, decode Play Integrity tokens and run these checks. They read a credential into memory when they need it and keep it there briefly:

WhatKept in a server's memory for at most
A push credential, decrypted5 minutes
The signed token sent to Apple, made with your key40 minutes, as Apple requires (20 to 60)
A Google access token, made with your service account50 minutes, and never past its expiry

Separate for each workspace. Each credential is named under your workspace, one per app. The dashboard's routes take the workspace from your sign-in, so no other workspace can name yours, and each Apple team's pushes travel on connections of their own.

Outside the platform, reading the store needs Graine's production cloud credentials. A service identity of their own for the services that use these credentials, whose access policy names only these secrets, is planned and not yet live.

Audited

Every change is a row in your Activity log. Each upload, replacement and removal writes one row to your workspace's Activity log: who did it, the role they held, what changed and whether it was allowed. An upload that Apple or Google refused is recorded too. The row names fields, never their values. A Check now row: Coming after SDK 0.14.4.

Every use is recorded. Coming after SDK 0.14.4. Each use of a credential is recorded with which credential, when, what for (a ring, an app verification or a check) and the result, kept for 90 days and shown on the credential's card. A use record carries no personal data: no device token, no user, no phone number and nothing about the conversation. Today each ring is visible as the scheduled call's status and in Call History, each app check is counted per day under What we've seen, and Graine's service logs record each use without the credential.

Health and alerts

When a push key stops working. If Apple or Google refuses your credential while Graine is ringing, Graine first checks the credential again by itself, so one device's bad token never switches your app off. If the check fails too, Graine:

  1. marks the credential Not working, with Apple's or Google's reason;
  2. sends each of your workspace's Admins one notification in the dashboard, once for that breakage however many rings fail;
  3. stops ringing that app until an Admin uploads a working credential.

When a Play Integrity key stops working, Graine marks it Not working with the reason. The notification to your Admins: Coming after SDK 0.14.4.

Delivery health on each card. Coming after SDK 0.14.4. When the credential was added and by whom, when it was last used, and over the last 7 days the rings sent, the failures by kind (a refused key, a dead device token, Apple or Google unavailable) and the last failure with its reason.

Data minimization

A ring carries no personal data. Every push Graine sends holds five values: type (agent_call), call_id, caller_name (the agent's call name), hosted ("1") and agent_id. No reason, no variables, no name, phone number or user ID. On iPhone it is a VoIP push with expiration 0, now or never; on Android a data message living 30 seconds. The agent learns why it is calling only when the call connects. What a ring carries.

Device push tokens. Your app's SDK registers each device's push token with Graine so it knows where to ring. Tokens are kept in Graine's primary database, which is encrypted at rest, and are also encrypted field by field where field-level encryption is provisioned for your deployment. A token is deleted when Apple or Google says it is dead, or 90 days after the device was last seen.

Integrity tokens are decoded and dropped. Neither the token nor its contents are stored or logged: the logs keep only the verdict words.

Rotate and revoke

Rotate without downtime: create the new credential at Apple or Google, Replace it in Graine (it is validated first, and the old one keeps working until the new one is saved), then revoke the old one at Apple or Google.

YouWhat happens
Press Remove in GraineThe credential is deleted from the store at once: it can no longer be read, and the store destroys it after a seven-day recovery window. Rings to that app stop within 5 minutes; calls that fall due after that end without ringing. For a Play Integrity credential, new proofs from that app are refused at once, and app passes already issued last until they expire, at most 24 hours
Revoke the APNs key at AppleApple refuses the next ring. Graine checks the key again, marks it Not working, notifies your Admins and stops ringing that app
Delete the service-account key in Google CloudGoogle stops accepting the key. An access token Graine took with it before then may keep working until it expires, at most 50 minutes later; then Graine marks the credential Not working and stops using it
Remove the service account's roleGoogle applies it within minutes, to tokens already issued too

A Play Integrity credential that stops working is treated like Google being unreachable: an install that proved itself less than seven days ago keeps its verdict, renewed an hour at a time, until those seven days run out. To refuse at once, also press Remove.

The least-privilege setup

Give Graine a credential that can do one thing, for one app.

An Apple push key for this app only

An APNs key can send pushes to every app in your Apple team unless you limit it. Create one for Graine alone: Apple Developer → Certificates, Identifiers & Profiles → Keys → + → tick Apple Push Notifications service (APNs) → Configure:

  1. Key restriction: Topic Specific, and select only this app's topics, including its VoIP topic, <bundle id>.voip, which is the one Graine sends on. A topic-specific key cannot push to your other apps.
  2. Environment: Production. A topic-specific key works in one environment; set Environment to Production for the app in Graine to match. Builds run from Xcode use the sandbox: test them with a separate sandbox key, in a workspace you keep for testing.
  3. Register → Download. The .p8 downloads once. Note the Key ID and your Team ID.
  4. Don't reuse the key your own servers send with, so that you can revoke Graine's key without touching anything else.

Save and check sends a VoIP push to that topic with a device token that belongs to nobody. If Apple refuses the topic, the dashboard says so and nothing is stored.

A Google service account that can only send messages

The key Firebase offers under Project settings → Service accounts → Generate new private key belongs to your project's Admin SDK account, which can administer your whole Firebase project. Don't give Graine that one. Instead:

  1. In Google Cloud, create a project just for this integration (for example acme-graine-push), and in it a service account (IAM & Admin → Service accounts → Create service account) with no role in that project. Firebase lets a service account from another project send messages for yours.
  2. In your Firebase project, IAM & Admin → IAM → Grant access: add the service account's email with the role Firebase Cloud Messaging API Admin (roles/firebasecloudmessaging.admin). For the strictest setup, grant instead a custom role holding only cloudmessaging.messages.create, the one permission a send needs. Grant nothing else. If Save and check says the messaging API is turned off, turn on the Firebase Cloud Messaging API in the project the dashboard names.
  3. Use keyless access when it is available, or add a JSON key to the service account (Keys → Add key → JSON) and upload it in Graine.

A Google service account for Play Integrity only

  1. Create a Google Cloud project just for this (for example acme-graine-integrity), and link it to your app in Play Console: Test and release → App integrity → Play Integrity API → Link a Cloud project. An app links one Cloud project: if your own servers already decode Play Integrity tokens, keep the project you have linked, and create a separate service account in it for Graine.
  2. In that project, enable the Play Integrity API (APIs & Services → Library).
  3. Create a service account with no IAM role. Decoding is authorised by the project's link to your app, not by a role. Save and check tells you if Google refuses it.
  4. Use keyless access when it is available, or add a JSON key and upload it.

With keyless access for both, your organization can also enforce Google's policy that forbids service-account keys (iam.disableServiceAccountKeyCreation).

Keyless access to Google

Coming after SDK 0.14.4

Not available yet. Until then, the Firebase and Play Integrity credentials need a service-account key.

Graine still sends every Android ring and decodes every integrity token, but holds no Google key. Instead, you let Graine's identity act as your least-privilege service account, through Google's workload identity federation. Graine runs on AWS, so the workload identity provider you create trusts Graine's AWS account. Inside that trust, each Graine workspace has an identity of its own, an assumed-role session named for that workspace:

arn:aws:sts::GRAINE_ACCOUNT_ID:assumed-role/GRAINE_ROLE/graine-YOUR_WORKSPACE

The dashboard shows the exact values for your workspace, filled into each command below.

What you set up, in the project that holds the service account:

  1. A workload identity pool for Graine:

    gcloud iam workload-identity-pools create graine \
      --location=global --display-name="Graine"
  2. A workload identity provider that trusts Graine's AWS account ID and, by its attribute condition, accepts only your workspace's identity:

    gcloud iam workload-identity-pools providers create-aws graine-aws \
      --location=global --workload-identity-pool=graine \
      --account-id=GRAINE_ACCOUNT_ID \
      --attribute-mapping="google.subject=assertion.arn" \
      --attribute-condition="assertion.arn == 'arn:aws:sts::GRAINE_ACCOUNT_ID:assumed-role/GRAINE_ROLE/graine-YOUR_WORKSPACE'"
  3. Permission for that one identity to act as that one service account:

    gcloud iam service-accounts add-iam-policy-binding SERVICE_ACCOUNT_EMAIL \
      --role=roles/iam.workloadIdentityUser \
      --member="principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/graine/subject/arn:aws:sts::GRAINE_ACCOUNT_ID:assumed-role/GRAINE_ROLE/graine-YOUR_WORKSPACE"
  4. The credential configuration file, which you upload in Graine instead of a key:

    gcloud iam workload-identity-pools create-cred-config \
      projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/graine/providers/graine-aws \
      --service-account=SERVICE_ACCOUNT_EMAIL --aws --enable-imdsv2 \
      --output-file=graine-google-keyless.json

What Graine keeps. The pool, the provider and the service account named in the file. The file holds no private key, and Google does not treat it as confidential. Graine never fetches the addresses inside it: it presents its own identity and talks only to Google's token and service-account endpoints.

What Graine checks on save. That the setup works: your workspace's identity can act as the service account, and the service account can send a validate_only message or decode a placeholder token. And that no other workspace could use it: Graine also tries with an identity that is not your workspace's, and refuses the setup if Google accepts that one too. A provider that trusts any session of Graine's account, or a binding for every identity in the pool, is refused with the reason.

Tokens. Each Google access token lasts at most an hour; Graine uses one for at most 50 minutes.

Revoke. Remove the roles/iam.workloadIdentityUser binding, or disable the provider or the pool: Google stops issuing tokens to Graine at once. A token already issued lasts until it expires, at most an hour; remove the service account's own role to stop it sooner. Remove in Graine deletes the setup.

Other credentials for in-app agents

  • iPhone app checks (App Attest). Graine keeps your app's bundle ID and Team ID, which are public, and each install's App Attest public key, to check that install's later proofs. There is no secret.
  • Verified users, with your sign-in's ID token. Graine keeps your JWKS URL, issuer, audience and user-ID claim, all public, and checks each token with the keys you publish. There is no secret.
  • Verified users, with a signed user ID. Graine generates the HMAC secret, shows it once, and keeps it like the credentials above. An agent has at most two, so you can rotate without downtime.
  • Website bot check. The secret of your bot-check provider (Cloudflare Turnstile or reCAPTCHA Enterprise), one per agent, is kept like the credentials above and never shown again; Graine uses it only to verify a visitor's check before a conversation starts.

Secure your agent and Website bot check cover these.

Coming after SDK 0.14.4

  • Keyless access to Google, for Android rings and Play Integrity.
  • Every use of a credential recorded, and shown on its card with delivery health and when it was last used.
  • Key ID, Team ID, environment, project ID, service-account email and who uploaded each credential, on its card.
  • Check now, with its own Activity log row.
  • A dashboard notification to your Admins when a Play Integrity credential stops working.

Planned, without a date: a KMS key reserved for these credentials on the shared platform, and a service identity of their own for the services that use them.

For the wider platform — encryption, tenancy, the audit trail, where data is processed and the status of certifications — see Security. Ask your Graine representative for anything on this page in writing.