暗号化とマスキングによるTwilio Voiceの入力の保護

Protect Twilio Voice Input with Encryption and Redaction
February 26, 2024
執筆者
レビュー担当者
Paul Kamp
Twilion

暗号化とマスキングによるTwilio Voiceの入力の保護

発信者が信頼して入力している機密情報を保護するために、できる限りの対策を講じていますか?

組織がより機密性の高い情報を活用するのに伴い、そのデータの保護はこれまで以上に重要になっています。Twilioは、機密データを保護するためのさまざまな方法を提供していますが、Twilioが責任を持って提供するリソースを実装するかどうかは貴社次第です。

この記事では、Twilioサーバーレス関数Voice PCIモード<Gather> TwiMLを使用し、Twilio Programmable Voiceから収集したデータの暗号化とマスキングを行う方法について説明します。

必要なもの

このチュートリアルを利用するには、以下の項目が必要です。

作成する内容

発信者認証を処理するためのシンプルな対話型音声アプリケーションを構築します。関数を使用し、<Gather> TwiMLを介して発信者に以下の機密情報を求めます。

  1. "お客様の4桁の暗証番号(PIN)を入力してください"

  2. "支払いカード番号の最後の4桁を入力してください"

発信者からこの情報を受信するとすぐに暗号化されます。その瞬間から、データは宛先に到達するまで暗号化された状態が保たれます。

実際の実装では、宛先は処理のバックエンドサービスになる可能性があります。ただし、ここでは、別の関数が「ダミーAPI」として機能し、復号化がどのように実行されるかを示します。

さらにVoice PCIモードを有効にし、Voice通話ログ内の収集された情報をマスキングします。

実装前

ソリューションの説明に入る前に、暗号化やマスキングを行わない場合ログがどのように表示されるかを確認します。

Twilio Functionsは、Functionで発生したあらゆるエラーをログとしてTwilio Debuggerに出力します。この例では、特定の数字が入力されていない場合にエラーをログに記録します。Debuggerで受信したエラーには、プレーンテキストのリクエストパラメーターが表示されます。

Programmable Voiceでは、Voice通話ログにも収集された数字がプレーンテキストで記録されます。

この情報は、通話ログまたはDebuggerにアクセスできる場合に参照できます。

実装後

このソリューションを実装した後に表示されるデータは、脆弱性が低くなります。最後に、Functionのログには、より安全で暗号化された値が表示されます。

通話ログには「*REDACTED*」(*マスキング済み*)と表示されます。

利用を開始する

Twilio Functions

この手順を実行するには、Twilio ConsoleのFunctionエディタを使用します。

経験豊富な開発者は、より強力なサーバーレスCLIを使用し、関数を作成、デプロイ、保守することを検討する必要があります。

関数は、サービス内で作成、格納されます。

  1. Twilio Consoleにログインし、[Functions](関数)タブに移動します。

  2. Create Service](サービスを作成)ボタンをクリックしてサービスを作成し、「encrypted-gather-sample」のような名前を付けます。

依存関係の追加

このソリューションでは、axiosライブラリを使用して架空のバックエンドサービス(decrypt-gather関数)にリクエストを送信し、処理を実行します。

axiosサービスの依存関係として追加します。

 

環境変数の作成

このソリューションでは、機密データの暗号化/復号化に使用する秘密鍵が必要です。

秘密鍵の文字列は、32バイト以上であることが必要です。この秘密鍵は秘密にしておきます。

ランダムな秘密鍵を作成するには、Mac/Linuxで以下のコマンドラインを使用します。

xxd -l32 -p /dev/urandom

この秘密鍵はNode.jsを使用して生成することもできます。

crypto.randomBytes(32).toString('hex')

 

鍵を格納する環境変数をサービス内に追加します。

テスト目的では、次の32バイトの秘密鍵を使用できます。

a154eb4c759711bc2538a7cc021e9e9f17dd8aa63151c62ca28a82a4a404203d

AES暗号化関数の作成

まず、対称鍵暗号を使用してデータの暗号化と復号を処理する関数を作成します。

Node.js Crypto

Node.jsは、Cryptoと呼ばれる組み込みの暗号化モジュールを提供しています。Cryptoには、createCipheriv()createDecipheriv()などの、使用するブロック暗号アルゴリズムの種類を指定できる便利なメソッドが複数用意されています。

GCMブロック暗号

AESと呼ばれるAdvanced Encryption Standardは、暗号化アルゴリズムを使用してデータを保護する技術です。AESは、さまざまな利用モードにより実現できます。

このソリューションでは、GCM(ガロアカウンターモード)を使用します。これは、速度と強度を高めるために推奨される対称鍵暗号方式のブロック暗号です。

コード

以下のコードを使用して、AESという名前の新しい関数を作成します。

 

const crypto = require("crypto")

const ALGORITHM = {
    BLOCK_CIPHER: "aes-256-gcm",
    AUTH_TAG_BYTE_SIZE: 16, 
    IV_BYTE_SIZE: 12,  
}

exports.encrypt = (plainText, key) => {
    const nonce = crypto.randomBytes(ALGORITHM.IV_BYTE_SIZE)
    const cipher = crypto.createCipheriv(
        ALGORITHM.BLOCK_CIPHER, 
        Buffer.from(key, 'hex'), 
        nonce, 
        {
            authTagLength: ALGORITHM.AUTH_TAG_BYTE_SIZE
        }
    )

    const cipherText = Buffer.concat([
        nonce,
        cipher.update(plainText),
        cipher.final(),
        cipher.getAuthTag()
    ])

    return cipherText.toString('hex')
}

exports.decrypt = (cipherText, key) => {
    cipherText = Buffer.from(cipherText, 'hex')

    const authTag = cipherText.slice(-16)
    const nonce = cipherText.slice(0, 12)
    const encryptedMessage = cipherText.slice(12, -16)

    const decipher = crypto.createDecipheriv(
        ALGORITHM.BLOCK_CIPHER, 
        Buffer.from(key), 
        nonce, 
        {
            authTagLength: ALGORITHM.AUTH_TAG_BYTE_SIZE
        }
    )

    decipher.setAuthTag(authTag)
    const decrypted = decipher.update(encryptedMessage, '', 'utf8') + decipher.final('utf8')      
    return decrypted 
}

この関数は、同じサービスの別の関数内からのみ使用されるため、可視性を「Private」に設定します。

encrypted-gather関数の作成

次に、機密性の高い<Gather>操作を実行する関数を作成します。この関数は、以降の手順で着信電話番号音声Webhookとして設定します。

この関数により、発信者が入力した番号は受信されるとすぐに暗号化され、暗号化された状態で最終的な「宛先」関数に送信されます。

コード

以下のコードを使用して、encrypted-gatherという名前の新しい関数を作成します。

const axios = require('axios')
const AES = require(Runtime.getFunctions()['AES'].path)

exports.handler = async function (context, event, callback) {
    const twiml = new Twilio.twiml.VoiceResponse()

    const secret_key = context.AES_SECRET

    const functionUrl = `https://${context.DOMAIN_NAME}/encrypted-gather`
    const dummyApi = `https://${context.DOMAIN_NAME}/decrypt-gather`

    const step = event.step || "getLast4CC"

    switch (step) {
        case ("getLast4CC"):
            gatherLast4Card(twiml, functionUrl);
            break
        case ("getPin"):
            let encryptedCardDigits = AES.encrypt(event.Digits, secret_key)
            gatherPin(twiml, encryptedCardDigits, functionUrl)
            break
        case ("processData"):
            let encryptedPinDigits = AES.encrypt(event.Digits, secret_key)
            await processGatheredData(twiml, event.encryptedCardDigits, encryptedPinDigits, dummyApi)
            break
    }

    return callback(null, twiml)
}

const gatherLast4Card = (twiml, functionUrl) => {
    const gather = twiml.gather({
        action: `${functionUrl}?step=getPin`,
        method: 'POST',
        input: 'dtmf',
        timeout: 10,
        numDigits: 4,
    });
    gather.say('Please enter last 4 digits of your payment card number.');

    return gather
}

const gatherPin = (twiml, encryptedCardDigits, functionUrl) => {
    const gather = twiml.gather({
        action: `${functionUrl}?step=processData&encryptedCardDigits=${encryptedCardDigits}`,
        method: 'POST',
        input: 'dtmf',
        timeout: 10,
        numDigits: 4,
    });
    gather.say('Please enter your unique 4 digit identification number');

    return gather
}

const processGatheredData = async (twiml, encryptedCardDigits, encryptedPinDigits, dummy_url) => {
    // make request to "dummy" api endpoint - example decrypt function
    try {
        const apiResponse = await axios({
            method: 'post',
            url: dummy_url,
            data: {
                encryptedCardDigits, encryptedPinDigits
            }
        })

        twiml.say(`Thank you. Your account number is ${apiResponse.data.account} and your balance is ${apiResponse.data.balance}`)
    }
    catch (e) {
        twiml.say(`We were not able to locate you in our system. Goodbye.`)
    }

    return twiml
}

この関数はTwilio内から呼び出され、X-Twilio-Signatureヘッダーで保護できるため、「Protected」に設定します。

このソリューションを本番環境に実装する場合は、復号化の「dummyApi」変数をバックエンドサービスのURLに変更する必要があります。

const dummyApi = `https://${context.DOMAIN_NAME}/decrypt-gather`

暗号化の方法

一番上にある以下の行では、前のステップで作成した関数をインポートしています。

const AES = require(Runtime.getFunctions()['AES'].path)

次に、環境変数から取得して、秘密鍵を定義します。

const secret_key = context.AES_SECRET

最も重要なポイントとして、すべての機密情報をencrypt関数でラップします。(この場合、<Gather>で収集された情報はDigitパラメーターとして渡され、イベントオブジェクトからアクセスできます。)

let encryptedCardDigits = AES.encrypt(event.Digits, secret_key)

これにより、収集された情報の暗号化が処理されます。

decrypt-gather関数の作成

最後に、機密データを復号化する方法を示す関数を作成します。

本番環境では、ビジネスニーズに基づいて発信者情報を処理するバックエンドサービスへの要求が発生する可能性があります。

このソリューションでは、3番目の関数がこのデータを処理する「バックエンドサービス」として機能します。この関数は、暗号化された数字を受け取り、さらに処理するために復号化します。

コード

以下のコードを使用して、decrypt-gatherという名前の新しい関数を作成します。

const AES = require(Runtime.getFunctions()['AES'].path) 

exports.handler = function(context, event, callback) { 
const response = new Twilio.Response() 
const secret_key = context.AES_SECRET 

const last4card = AES.decrypt(event.encryptedCardDigits, secret_key) 
const pin = AES.decrypt(event.encryptedPinDigits, secret_key) 

//hard-coded values used for testing purposes 
if (last4card === "1234" && pin === "4321") { 
response.setBody(JSON.stringify({ 
account: "AC12345678", 
balance: "12.55"
 })) 
} else { 
response.setStatusCode(404) 
response.setBody("No data found") 
} 

return callback(null, response) 
}

この関数は架空の外部サービスとしての役割を果たすため、可視性を「Public」に設定します。

復号化の方法

一番上で、再度AES関数をインポートし、変数としてsecret_keyを定義します。

次に、先ほど暗号化した情報に対してdecryptを呼び出します。

 

const last4card = AES.decrypt(event.encryptedCardDigits, secret_key)

そのほかの構成

電話番号Webhook

簡単にするために、この関数を電話番号に直接接続します。

電話番号を設定するには、次の手順を実行します。

  1. Twilio Consoleから、[Phone Numbers](電話番号)セクションに移動します。

  2. 電話番号を選択し、[Voice & Fax](音声とファックス)セクションまでスクロールします。

  3. Voice Configuration](音声設定)の[A call comes in](着信)Webhookとしてencrypted-gather関数を設定します。

  4. 変更内容を保存します。

Twilio Studioからこれをトリガーしたい場合は、このブログ記事を参照して、このソリューションをStudioに安全に組み込む方法を確認してください。

PCIモードを有効にする

あと少しで完了です。関数の安全性は確保できましたが、Twilioには収集された数字がプレーンテキストで残される場所がもう1つあります。それがVoice通話ログです。

以下は、暗号化された<Gather>ソリューションが実装された着信通話のTwilio Consoleからのスクリーンショットです。関数ではデータが保護されていますが、Voiceでは保護されていません。

PCIモードを使用すると、このデータを通話ログに表示しないようにできます。アカウントでPCIモードを有効にすると、<Gather>操作でキャプチャされたすべてのデータがマスキングされます。

アカウントでPCIモードを有効にする操作は、やり直すことができません。つまり、一度有効にすると、無効にすることはできません。マスキングすると、Voiceの問題のトラブルシューティングがより困難になる場合があります。

機密情報を安全にキャプチャすることに本気で取り組んでいる場合は、

  1. Twilio ConsoleでTwilio Voice設定に移動します。(左側のナビゲーションペインで、[Voice](Voice) > [Settings](設定) > [General](全般)をクリックします。)

  2. [Enable PCI Mode](PCIモードを有効にする)ボタンをクリックします。

  3. 変更内容を保存します。

通話の発信

いよいよ最後の審判となりました。電話番号にテスト通話を発信しましょう。

ここからは2つの経路があります。

「クレジットカード」の下4桁に1234を、固有のPINに4321を入力した場合、通話でダミーのアカウント情報が返されるのが聞こえます。これは、成功したAPI応答の例です。

他の数字を入力すると、既知のユーザーではない動作となり、404の応答が返されます。これは、失敗したリクエストの例で、Twilio Debuggerにエラーが記録されます。

動作確認方法

失敗する場合の経路をたどり、Twilio Consoleでエラーログを確認します。

404のエラー応答が返された場合、Functionsの82005エラーと以下の詳細が表示されます。

この結果は良好です。暗号化がないと、応答が失敗した場合、これらの変数がプレーンテキストで記録されます。しかしここでは、データはより安全で暗号化された形でログに記録されます。

さらに通話ログに数字が「*REDACTED*」(*マスキング済み*)と表示されていることも確認できます。

これで安全でしょうか?

(オプションのPCIモードの手順を含め)このチュートリアルに従うと、データがTwilioのエコシステム内のすべての場所でプレーンテキストでログに記録されることがなくなります。Twilioの誰も機密データを復号化することはできないため、デフォルト状態よりもセキュリティが強化されています。

ただし、暗号化と復号化に使用される秘密鍵は、サービス上の環境変数として保存されています。つまり、Twilio Functionsへのアクセスを許可したユーザーは、鍵を抽出し、値の復号化を試みることが可能です。

最終的な推奨事項

提供されているサンプルコードを変更する場合、Functionsはコンソールの警告とエラーを内部TwilioシステムとTwilio Debugger一定期間保持することに注意してください。

機密性の高い暗号化されていないデータでは、次のコンソールロギング方法を使用しないでください。

 

console.log() 
console.warn() 
console.error()

まとめ

このレッスンでは、<Gather> TwiMLから収集されたデータを、サーバーレス関数による暗号化とVoice PCIモードによるマスキングで保護する方法を学習しました。

発信者から支払いを受け取りたい場合は、完全にPCI準拠のTwilio <Pay>機能を検討してください。

TwilioのPCIコンプライアンスの詳細については、ドキュメントと責任マトリックスをご覧ください。

ユーザーは、秘密が守られると信頼して機密情報を入力します。処理するデータを保護するためにできる限りの対策を行うことで、その信頼を尊重し、維持することを確認してください。

Bry Schininaは、個人情報を公開しない企業に高く評価されている開発者兼教育者です。彼女はTwilioのテクニカルリード兼シニアテクニカルアカウントマネージャーとして、複雑な問題を解決し、デジタルエンゲージメントプラットフォームでの組織の成功を支援しています。連絡先はbschinina [at] twilio.comです。