Skip to content

Security Guide

Essential security practices for building secure messaging applications with MsGine.

API Key Security

Never Expose Keys

API keys must never be exposed in:

  • Client-side code: Browser JavaScript, mobile apps
  • Version control: Git repositories, especially public ones
  • Log files: Application logs, error messages
  • Error responses: API error responses sent to clients
  • Screenshots: Documentation, support tickets

Use Environment Variables

Store keys securely using environment variables:

typescript
// ❌ NEVER do this
const client = new MsGineClient({
  apiKey: 'msgine_live_abc123...'
})

// ✅ Always use environment variables
const client = new MsGineClient({
  apiKey: process.env.MSGINE_API_KEY!,
})

.env File

bash
# .env
MSGINE_API_KEY=your_api_key_here
MSGINE_WEBHOOK_SECRET=your_webhook_secret_here

Important: Add .env to .gitignore:

bash
# .gitignore
.env
.env.local
.env.*.local

Use Secret Management Systems

For production environments, use dedicated secret management:

AWS Secrets Manager

typescript
import { SecretsManager } from '@aws-sdk/client-secrets-manager'

const client = new SecretsManager({ region: 'us-east-1' })

async function getApiKey(): Promise<string> {
  const response = await client.getSecretValue({
    SecretId: 'msgine/api-key'
  })

  return response.SecretString!
}

Azure Key Vault

typescript
import { SecretClient } from '@azure/keyvault-secrets'
import { DefaultAzureCredential } from '@azure/identity'

const credential = new DefaultAzureCredential()
const client = new SecretClient(
  'https://your-vault.vault.azure.net',
  credential
)

async function getApiKey(): Promise<string> {
  const secret = await client.getSecret('msgine-api-key')
  return secret.value!
}

Google Cloud Secret Manager

typescript
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'

const client = new SecretManagerServiceClient()

async function getApiKey(): Promise<string> {
  const [version] = await client.accessSecretVersion({
    name: 'projects/my-project/secrets/msgine-api-key/versions/latest'
  })

  return version.payload!.data!.toString()
}

Key Rotation

Rotate API keys regularly:

  1. Generate a new key in the Developer Dashboard
  2. Update your secret management system
  3. Deploy the new key to your application
  4. Verify the new key works
  5. Revoke the old key

Recommended rotation schedule:

  • Production: Every 90 days
  • Development: As needed
  • Compromised: Immediately

Webhook Security

Verify Signatures

Always verify webhook signatures to prevent spoofing:

typescript
import crypto from 'crypto'

function verifyWebhookSignature(req: Request): boolean {
  const signature = req.headers['x-msgine-signature'] as string
  const secret = process.env.MSGINE_WEBHOOK_SECRET!

  const payload = JSON.stringify(req.body)
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

app.post('/webhooks/msgine', (req, res) => {
  if (!verifyWebhookSignature(req)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // Process webhook
  // ...
})

Use HTTPS Only

Configure webhooks to use HTTPS URLs only:

typescript
// ❌ Insecure
callbackUrl: 'http://myapp.com/webhook'

// ✅ Secure
callbackUrl: 'https://myapp.com/webhook'

MsGine will reject HTTP webhook URLs.

IP Whitelisting

Restrict webhook requests to MsGine's IP addresses:

typescript
const MSGINE_IPS = [
  '52.89.214.238',
  '34.212.75.30',
  '54.218.53.128'
]

function isValidMsGineIP(ip: string): boolean {
  return MSGINE_IPS.includes(ip)
}

app.post('/webhooks/msgine', (req, res) => {
  const clientIP = req.ip

  if (!isValidMsGineIP(clientIP)) {
    return res.status(403).json({ error: 'Forbidden' })
  }

  // Process webhook
  // ...
})

Implement Idempotency

Prevent duplicate webhook processing:

typescript
const processedWebhooks = new Set<string>()

app.post('/webhooks/msgine', (req, res) => {
  const webhookId = req.body.id

  if (processedWebhooks.has(webhookId)) {
    console.log('Duplicate webhook, skipping')
    return res.status(200).json({ received: true })
  }

  processedWebhooks.add(webhookId)

  // Process webhook
  // ...
})

Input Validation

Validate Phone Numbers

Always validate phone numbers before sending:

typescript
function validatePhoneNumber(phone: string): boolean {
  // E.164 format: +[1-15 digits]
  const e164Regex = /^\+[1-9]\d{1,14}$/

  if (!e164Regex.test(phone)) {
    throw new Error('Phone number must be in E.164 format')
  }

  return true
}

async function sendSms(to: string, message: string) {
  validatePhoneNumber(to)
  return await client.sms.send({ to, message })
}

Sanitize Message Content

Remove potentially harmful content:

typescript
function sanitizeMessage(message: string): string {
  // Remove control characters
  let sanitized = message.replace(/[\x00-\x1F\x7F]/g, '')

  // Trim whitespace
  sanitized = sanitized.trim()

  // Validate length
  if (sanitized.length === 0) {
    throw new Error('Message cannot be empty')
  }

  if (sanitized.length > 1000) {
    throw new Error('Message exceeds maximum length of 1000 characters')
  }

  return sanitized
}

async function sendSms(to: string, message: string) {
  const sanitizedMessage = sanitizeMessage(message)
  return await client.sms.send({ to, message: sanitizedMessage })
}

Prevent Injection Attacks

Validate and escape user input:

typescript
function escapeMessage(message: string): string {
  // Escape potentially dangerous characters
  return message
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
}

Rate Limiting

Implement Application-Level Rate Limiting

Protect your application from abuse:

typescript
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per window
  message: 'Too many requests, please try again later'
})

app.post('/api/send-sms', limiter, async (req, res) => {
  // Handle SMS sending
})

Per-User Rate Limiting

Implement per-user limits:

typescript
import Redis from 'ioredis'

const redis = new Redis()

async function checkUserRateLimit(userId: string): Promise<boolean> {
  const key = `rate_limit:${userId}`
  const count = await redis.incr(key)

  if (count === 1) {
    await redis.expire(key, 3600) // 1 hour
  }

  const limit = 100 // 100 messages per hour per user
  return count <= limit
}

app.post('/api/send-sms', async (req, res) => {
  const userId = req.user.id

  if (!await checkUserRateLimit(userId)) {
    return res.status(429).json({ error: 'Rate limit exceeded' })
  }

  // Send SMS
})

Data Protection

Encrypt Sensitive Data

Encrypt phone numbers and message content in your database:

typescript
import crypto from 'crypto'

const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY! // 32 bytes
const IV_LENGTH = 16

function encrypt(text: string): string {
  const iv = crypto.randomBytes(IV_LENGTH)
  const cipher = crypto.createCipheriv(
    'aes-256-cbc',
    Buffer.from(ENCRYPTION_KEY),
    iv
  )

  let encrypted = cipher.update(text)
  encrypted = Buffer.concat([encrypted, cipher.final()])

  return iv.toString('hex') + ':' + encrypted.toString('hex')
}

function decrypt(text: string): string {
  const parts = text.split(':')
  const iv = Buffer.from(parts[0], 'hex')
  const encrypted = Buffer.from(parts[1], 'hex')

  const decipher = crypto.createDecipheriv(
    'aes-256-cbc',
    Buffer.from(ENCRYPTION_KEY),
    iv
  )

  let decrypted = decipher.update(encrypted)
  decrypted = Buffer.concat([decrypted, decipher.final()])

  return decrypted.toString()
}

Implement Data Retention Policies

Automatically delete old message data:

typescript
async function deleteOldMessages() {
  const thirtyDaysAgo = new Date()
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)

  await db.messages.deleteMany({
    createdAt: { $lt: thirtyDaysAgo }
  })
}

// Run daily
setInterval(deleteOldMessages, 24 * 60 * 60 * 1000)

Compliance

GDPR Compliance

For EU users:

  1. Consent: Obtain explicit consent before sending messages
  2. Right to erasure: Allow users to delete their data
  3. Data portability: Provide user data exports
  4. Privacy policy: Clearly explain data usage
typescript
async function checkConsent(userId: string): Promise<boolean> {
  const consent = await db.consents.findOne({
    userId,
    consentType: 'sms',
    consentGiven: true
  })

  return !!consent
}

async function sendSms(userId: string, message: string) {
  if (!await checkConsent(userId)) {
    throw new Error('User has not consented to SMS messages')
  }

  const user = await db.users.findById(userId)
  return await client.sms.send({
    to: user.phone,
    message
  })
}

TCPA Compliance (US)

For US users:

  1. Prior express consent: Get written consent
  2. Opt-out: Provide easy opt-out mechanism
  3. Hours: Don't send between 9 PM - 8 AM
  4. Identification: Identify your organization
typescript
function isValidSendingTime(): boolean {
  const hour = new Date().getHours()
  return hour >= 8 && hour < 21 // 8 AM - 9 PM
}

async function sendSmsCompliant(to: string, message: string) {
  if (!isValidSendingTime()) {
    throw new Error('Cannot send SMS outside 8 AM - 9 PM')
  }

  // Add organization identifier
  const compliantMessage = `${message}\n\nReply STOP to opt out`

  return await client.sms.send({ to, message: compliantMessage })
}

Monitoring and Alerting

Log Security Events

Log all security-related events:

typescript
import winston from 'winston'

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'security.log' })
  ]
})

// Log failed authentication
logger.warn('Webhook signature verification failed', {
  ip: req.ip,
  timestamp: new Date().toISOString()
})

Incident Response

Have a Security Incident Plan

  1. Detect: Monitor for suspicious activity
  2. Contain: Revoke compromised keys immediately
  3. Investigate: Review logs to understand the breach
  4. Recover: Generate new keys and update systems
  5. Learn: Document and improve security measures

Key Compromise Response

If a key is compromised:

typescript
async function handleKeyCompromise() {
  // 1. Revoke compromised key immediately
  await revokeKey(compromisedKey)

  // 2. Generate new key
  const newKey = await generateNewKey()

  // 3. Update secret management
  await updateSecret('msgine-api-key', newKey)

  // 4. Alert team
  await alertTeam('API key compromised and rotated')

  // 5. Review logs
  await reviewAuditLogs(compromisedKey)
}

Next Steps

Released under the MIT License.