Webrtc-security

提供:Dev Guides
移動先:案内検索

WebRTC-セキュリティ

この章では、「WebRTCシグナリング」の章で作成したシグナリングサーバーにセキュリティ機能を追加します。 2つの機能強化があります-

  • Redisデータベースを使用したユーザー認証
  • セキュアソケット接続を有効にする

まず、Redisをインストールする必要があります。

  • 最新の安定版リリースをhttp://redis.io/download(私の場合は3.05)からダウンロードします
  • それを開梱する
  • ダウンロードしたフォルダー内で_sudo make install_を実行します
  • インストールが完了したら、_make test_を実行して、すべてが正常に機能しているかどうかを確認します。

Redisには2つの実行可能なコマンドがあります-

  • redis-cli -Redisのコマンドラインインターフェイス(クライアント部分)
  • redis-server -Redisデータストア

Redisサーバーを実行するには、ターミナルコンソールで_redis-server_と入力します。 次が表示されるはずです-

Redis Server

次に、新しいターミナルウィンドウを開き、_redis-cli_を実行してクライアントアプリケーションを開きます。

Redis-cli

基本的に、Redisはキーと値のデータベースです。 文字列値でキーを作成するには、SETコマンドを使用する必要があります。 キー値を読み取るには、GETコマンドを使用する必要があります。 2人のユーザーとパスワードを追加しましょう。 キーはユーザー名になり、これらのキーの値は対応するパスワードになります。

ユーザーとパスワードを追加

ここで、ユーザー認証を追加するためにシグナリングサーバーを変更する必要があります。 _server.js_ファイルの先頭に次のコードを追加します-

//require the redis library in Node.js
var redis = require("redis");

//creating the redis client object
var redisClient = redis.createClient();

上記のコードでは、Node.js用のRedisライブラリと、サーバー用のredisクライアントの作成が必要です。

認証を追加するには、接続オブジェクトの_message_ハンドラを変更します-

//when a user connects to our sever
wss.on('connection', function(connection) {
   console.log("user connected");

  //when server gets a message from a connected user
   connection.on('message', function(message) {

      var data;
     //accepting only JSON messages
      try {
         data = JSON.parse(message);
      } catch (e) {
         console.log("Invalid JSON");
         data = {};
      }

     //check whether a user is authenticated
      if(data.type != "login") {

        //if user is not authenticated
         if(!connection.isAuth) {
            sendTo(connection, {
               type: "error",
               message: "You are not authenticated"
            });
            return;
         }
      }

     //switching type of the user message
      switch (data.type) {
        //when a user tries to login
         case "login":
            console.log("User logged:", data.name);
           //get password for this username from redis database

            redisClient.get(data.name, function(err, reply) {
              //check if password matches with the one stored in redis
               var loginSuccess = reply === data.password;

              //if anyone is logged in with this username or incorrect password
                  then refuse
               if(users[data.name] || !loginSuccess) {
                  sendTo(connection, {
                     type: "login",
                     success: false
                  });
               } else {
                 //save user connection on the server
                  users[data.name] = connection;
                  connection.name = data.name;
                  connection.isAuth = true;

                  sendTo(connection, {
                     type: "login",
                     success: true
                  });
               }
            });

            break;
      }
   });

}

//...
//*****other handlers*******

上記のコードでは、ユーザーがログインを試みた場合、Redisからパスワードを取得し、保存されているパスワードと一致するかどうかを確認し、成功した場合はサーバーにユーザー名を保存します。 また、接続に_isAuth_フラグを追加して、ユーザーが認証されているかどうかを確認します。 このコードに注意してください-

//check whether a user is authenticated
if(data.type != "login") {

  //if user is not authenticated
   if(!connection.isAuth) {
      sendTo(connection, {
         type: "error",
         message: "You are not authenticated"
      });

      return;
   }
}

認証されていないユーザーがオファーを送信しようとしたり、接続を離れようとした場合、エラーが返されます。

次のステップは、セキュアソケット接続を有効にすることです。 WebRTCアプリケーションに強くお勧めします。 PKI(公開キー基盤)は、CA(認証局)からのデジタル署名です。 次に、ユーザーは、証明書の署名に使用される秘密キーがCAの証明書の公開キーと一致することを確認します。 開発目的のため。 自己署名セキュリティ証明書を使用します。

opensslを使用します。 SSL(Secure Sockets Layer)およびTLS(Transport Layer Security)プロトコルを実装するオープンソースのツールです。 多くの場合、デフォルトでUnixシステムにインストールされます。 _openssl version -a_を実行して、インストールされているかどうかを確認します。

Opensslを使用

公開および秘密のセキュリティ証明書キーを生成するには、以下に示す手順に従う必要があります-

  • 一時サーバーパスワードキーを生成
openssl genrsa -des3 -passout pass:x -out server.pass.key 2048

一時サーバーのパスワードキー

  • サーバー秘密鍵を生成
openssl rsa -passin pass:12345 -in server.pass.key -out server.key

サーバー秘密鍵

  • 署名要求を生成します。 会社について追加の質問があります。 常に「Enter」ボタンを押すだけです。
openssl req -new -key server.key -out server.csr

署名リクエストの生成

  • 証明書を生成
openssl x509 -req -days 1095 -in server.csr -signkey server.key -out server.crt

証明書の生成

これで、証明書(server.crt)と秘密鍵(server.key)の2つのファイルができました。 それらをシグナリングサーバーのルートフォルダにコピーします。

セキュアソケット接続を有効にするには、シグナルサーバーを変更します。

//require file system module
var fs = require('fs');
var httpServ = require('https');

//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

//out secure server will bind to the port 9090
var cfg = {
   port: 9090,
   ssl_key: 'server.key',
   ssl_cert: 'server.crt'
};

//in case of http request just send back "OK"
var processRequest = function(req, res) {
   res.writeHead(200);
   res.end("OK");
};

//create our server with SSL enabled
var app = httpServ.createServer({
   key: fs.readFileSync(cfg.ssl_key),
   cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);

//require our websocket library
var WebSocketServer = require('ws').Server;

//creating a websocket server at port 9090
var wss = new WebSocketServer({server: app});

//all connected to the server users
var users = {};

//require the redis library in Node.js
var redis = require("redis");

//creating the redis client object
var redisClient = redis.createClient();

//when a user connects to our sever
wss.on('connection', function(connection){
//...other code

上記のコードでは、プライベートキーと証明書を読み取るために_fs_ライブラリを必要とし、プライベートキーと証明書のバインディングポートとパスを使用して_cfg_オブジェクトを作成します。 次に、ポート9090でキーとWebSocketサーバーを使用してHTTPSサーバーを作成します。

Operaで [[1]] を開きます。 次が表示されるはずです-

無効な証明書

[続行]ボタンをクリックします。 「OK」メッセージが表示されます。

安全な信号サーバーをテストするために、「WebRTC Text Demo」チュートリアルで作成したチャットアプリケーションを変更します。 パスワードフィールドを追加するだけです。 以下は_indexl_ファイル全体です-

<html>

   <head>
      <title>WebRTC Text Demo</title>
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/>
   </head>

   <style>
      body {
         background: #eee;
         padding: 5% 0;
      }
   </style>

   <body>
      <div id = "loginPage" class = "container text-center">

         <div class = "row">
            <div class = "col-md-4 col-md-offset-4">
               <h2>WebRTC Text Demo. Please sign in</h2>
               <label for = "usernameInput" class = "sr-only">Login</label>
               <input type = "email" id = "usernameInput"
                  class = "form-control formgroup" placeholder = "Login"
                  required = "" autofocus = "">
               <input type = "text" id = "passwordInput"
                  class = "form-control form-group" placeholder = "Password"
                  required = "" autofocus = "">
               <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock"
                  >Sign in</button>
            </div>
         </div>

      </div>

      <div id = "callPage" class = "call-page container">

         <div class = "row">
            <div class = "col-md-4 col-md-offset-4 text-center">
               <div class = "panel panel-primary">
                  <div class = "panel-heading">Text chat</div>
                  <div id = "chatarea" class = "panel-body text-left"></div>
               </div>
            </div>
         </div>

         <div class = "row text-center form-group">
            <div class = "col-md-12">
               <input id = "callToUsernameInput" type = "text"
                  placeholder = "username to call"/>
               <button id = "callBtn" class = "btn-success btn">Call</button>
               <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button>
            </div>
         </div>

         <div class = "row text-center">
            <div class = "col-md-12">
               <input id = "msgInput" type = "text" placeholder = "message"/>
               <button id = "sendMsgBtn" class = "btn-success btn">Send</button>
            </div>
         </div>

      </div>

      <script src = "client.js"></script>

   </body>

</html>

この行を使用して、_client.js_ファイルでセキュアソケット接続を有効にする必要もあります_var conn = new WebSocket( 'wss://localhost:9090'); _ _wss_プロトコルに注意してください。 次に、ログインボタンハンドラは、ユーザー名とともにパスワードを送信するように変更する必要があります-

loginBtn.addEventListener("click", function (event) {
   name = usernameInput.value;
   var pwd = passwordInput.value;

   if (name.length > 0) {
      send({
         type: "login",
         name: name,
         password: pwd
      });
   }

});

以下は_client.js_ファイル全体です-

//our username
var name;
var connectedUser;

//connecting to our signaling server
var conn = new WebSocket('wss://localhost:9090');

conn.onopen = function () {
   console.log("Connected to the signaling server");
};

//when we got a message from a signaling server
conn.onmessage = function (msg) {
   console.log("Got message", msg.data);

   var data = JSON.parse(msg.data);

   switch(data.type) {
      case "login":
         handleLogin(data.success);
         break;
     //when somebody wants to call us
      case "offer":
         handleOffer(data.offer, data.name);
         break;
      case "answer":
         handleAnswer(data.answer);
         break;
     //when a remote peer sends an ice candidate to us
      case "candidate":
         handleCandidate(data.candidate);
         break;
      case "leave":
         handleLeave();
         break;
      default:
         break;
   }
};

conn.onerror = function (err) {
   console.log("Got error", err);
};

//alias for sending JSON encoded messages
function send(message) {
  //attach the other peer username to our messages
   if (connectedUser) {
      message.name = connectedUser;
   }

   conn.send(JSON.stringify(message));
};

//******
//UI selectors block
//******

var loginPage = document.querySelector('#loginPage');
var usernameInput = document.querySelector('#usernameInput');
var passwordInput = document.querySelector('#passwordInput');
var loginBtn = document.querySelector('#loginBtn');

var callPage = document.querySelector('#callPage');
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
var hangUpBtn = document.querySelector('#hangUpBtn');

var msgInput = document.querySelector('#msgInput');
var sendMsgBtn = document.querySelector('#sendMsgBtn');
var chatArea = document.querySelector('#chatarea');

var yourConn;
var dataChannel;

callPage.style.display = "none";

//Login when the user clicks the button
loginBtn.addEventListener("click", function (event) {
   name = usernameInput.value;
   var pwd = passwordInput.value;

   if (name.length > 0) {
      send({
         type: "login",
         name: name,
         password: pwd
      });
   }

});

function handleLogin(success) {
   if (success === false) {
      alert("Ooops...incorrect username or password");
   } else {
      loginPage.style.display = "none";
      callPage.style.display = "block";

     //**********************
     //Starting a peer connection
     //**********************

     //using Google public stun server
      var configuration = {
         "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }]
      };

      yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]});

     //Setup ice handling
      yourConn.onicecandidate = function (event) {
         if (event.candidate) {
            send({
               type: "candidate",
               candidate: event.candidate
            });
         }
      };

     //creating data channel
      dataChannel = yourConn.createDataChannel("channel1", {reliable:true});

      dataChannel.onerror = function (error) {
         console.log("Ooops...error:", error);
      };

     //when we receive a message from the other peer, display it on the screen
      dataChannel.onmessage = function (event) {
         chatArea.innerHTML += connectedUser + ": " + event.data + "<br/>";
      };

      dataChannel.onclose = function () {
         console.log("data channel is closed");
      };
   }

};

//initiating a call
callBtn.addEventListener("click", function () {
   var callToUsername = callToUsernameInput.value;

   if (callToUsername.length > 0) {

      connectedUser = callToUsername;

     //create an offer
      yourConn.createOffer(function (offer) {
         send({
            type: "offer",
            offer: offer
         });

         yourConn.setLocalDescription(offer);

      }, function (error) {
         alert("Error when creating an offer");
      });
   }
});

//when somebody sends us an offer
function handleOffer(offer, name) {
   connectedUser = name;
   yourConn.setRemoteDescription(new RTCSessionDescription(offer));

  //create an answer to an offer
   yourConn.createAnswer(function (answer) {
      yourConn.setLocalDescription(answer);

      send({
         type: "answer",
         answer: answer
      });

   }, function (error) {
      alert("Error when creating an answer");
   });

};

//when we got an answer from a remote user
function handleAnswer(answer) {
   yourConn.setRemoteDescription(new RTCSessionDescription(answer));
};

//when we got an ice candidate from a remote user
function handleCandidate(candidate) {
   yourConn.addIceCandidate(new RTCIceCandidate(candidate));
};

//hang up
hangUpBtn.addEventListener("click", function () {

   send({
      type: "leave"
   });

   handleLeave();
});

function handleLeave() {
   connectedUser = null;
   yourConn.close();
   yourConn.onicecandidate = null;
};

//when user clicks the "send message" button
sendMsgBtn.addEventListener("click", function (event) {
   var val = msgInput.value;
   chatArea.innerHTML += name + ": " + val + "<br/>";

  //sending a message to a connected peer
   dataChannel.send(val);
   msgInput.value = "";
});

ここで、_node server_を介して安全なシグナリングサーバーを実行します。 変更されたチャットデモフォルダー内で_node static_を実行します。 2つのブラウザタブで localhost:8080 を開きます。 ログインしてみてください。 「password1」を含む「user1」と「password2」を含む「user2」のみがログインできることを覚えておいてください。 次に、RTCPeerConnectionを確立し(別のユーザーを呼び出す)、メッセージの送信を試みます。

RTCPeerConnectionを確立

以下は私たちの安全な信号サーバーの全体のコードです-

//require file system module
var fs = require('fs');
var httpServ = require('https');

//https://github.com/visionmedia/superagent/issues/205
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

//out secure server will bind to the port 9090
var cfg = {
   port: 9090,
   ssl_key: 'server.key',
   ssl_cert: 'server.crt'
};

//in case of http request just send back "OK"
var processRequest = function(req, res){
   res.writeHead(200);
   res.end("OK");
};

//create our server with SSL enabled
var app = httpServ.createServer({
   key: fs.readFileSync(cfg.ssl_key),
   cert: fs.readFileSync(cfg.ssl_cert)
}, processRequest).listen(cfg.port);

//require our websocket library
var WebSocketServer = require('ws').Server;

//creating a websocket server at port 9090
var wss = new WebSocketServer({server: app});

//all connected to the server users
var users = {};

//require the redis library in Node.js
var redis = require("redis");

//creating the redis client object
var redisClient = redis.createClient();

//when a user connects to our sever
wss.on('connection', function(connection) {
   console.log("user connected");

  //when server gets a message from a connected user
   connection.on('message', function(message) {

      var data;
     //accepting only JSON messages
      try {
         data = JSON.parse(message);
      } catch (e) {
         console.log("Invalid JSON");
         data = {};
      }

     //check whether a user is authenticated
      if(data.type != "login") {
        //if user is not authenticated
         if(!connection.isAuth) {
            sendTo(connection, {
               type: "error",
               message: "You are not authenticated"
            });

            return;
         }
      }

     //switching type of the user message
      switch (data.type) {
        //when a user tries to login
         case "login":
            console.log("User logged:", data.name);
           //get password for this username from redis database
            redisClient.get(data.name, function(err, reply) {

              //check if password matches with the one stored in redis
               var loginSuccess = reply === data.password;

              //if anyone is logged in with this username or incorrect password
                  then refuse
               if(users[data.name] || !loginSuccess) {
                  sendTo(connection, {
                     type: "login",
                     success: false
                  });
               } else {
                 //save user connection on the server
                  users[data.name] = connection;
                  connection.name = data.name;
                  connection.isAuth = true;

                  sendTo(connection, {
                     type: "login",
                     success: true
                  });
               }
            });

            break;

         case "offer":
           //for ex. UserA wants to call UserB
            console.log("Sending offer to: ", data.name);

           //if UserB exists then send him offer details
            var conn = users[data.name];

            if(conn != null) {
              //setting that UserA connected with UserB
               connection.otherName = data.name;

               sendTo(conn, {
                  type: "offer",
                  offer: data.offer,
                  name: connection.name
               });
            }

            break;

         case "answer":
            console.log("Sending answer to: ", data.name);
           //for ex. UserB answers UserA
            var conn = users[data.name];

            if(conn != null) {
               connection.otherName = data.name;

               sendTo(conn, {
                  type: "answer",
                  answer: data.answer
               });
            }

            break;

         case "candidate":
            console.log("Sending candidate to:",data.name);
            var conn = users[data.name];

            if(conn != null) {
               sendTo(conn, {
                  type: "candidate",
                  candidate: data.candidate
               });
            }

            break;

         case "leave":
            console.log("Disconnecting from", data.name);
            var conn = users[data.name];
            conn.otherName = null;

           //notify the other user so he can disconnect his peer connection
            if(conn != null) {
               sendTo(conn, {
                  type: "leave"
               });
            }

            break;

         connection.on("close", function() {

            if(connection.name) {
               delete users[connection.name];

               if(connection.otherName) {
                  console.log("Disconnecting from ", connection.otherName);
                  var conn = users[connection.otherName];
                  conn.otherName = null;

                  if(conn != null) {
                     sendTo(conn, {
                        type: "leave"
                    });
                  }

               }
            }
         });

         default:
            sendTo(connection, {
               type: "error",
               message: "Command no found: " + data.type
            });

            break;
      }
   });

  //when user exits, for example closes a browser window
  //this may help if we are still in "offer","answer" or "candidate" state
   connection.on("close", function() {
      if(connection.name) {
         delete users[connection.name];
      }
   });

   connection.send("Hello from server");
});

function sendTo(connection, message) {
   connection.send(JSON.stringify(message));
}

概要

この章では、シグナリングサーバーにユーザー認証を追加しました。 また、自己署名SSL証明書を作成し、WebRTCアプリケーションの範囲でそれらを使用する方法も学びました。