03 May 2009

(PHP) GnuPGを利用する

PHPでGnuPG (gpg) を用いる方法のメモ

PHPの標準関数proc_open( ) を用いて、gpgコマンドを直接呼び出す例。PearにCrypt_GPGライブラリが存在するが、「秘密鍵が無い、公開鍵だけの状態で暗号化できない」という不具合があるため、直接gpgコマンドを使うことにした。


プロセスを作成しUNIX/Linux上でプログラムを起動するためのコア関数
$cmdがシェルに渡すコマンドライン。 (gpgじゃなくても、どんなプログラムでも起動可能)
例 $cmd = '/usr/local/bin/gpg --list-keys';


function exec_gpg($cmd)
{
$descriptorspec = array(
0 => array('pipe', 'r'), // stdin
1 => array('pipe', 'w'), // stdout
2 => array('pipe', 'w') // stderr
);

// プロセスを開始する(プログラムが実行される)
$process = proc_open($cmd, $descriptorspec, $pipes);

fclose($pipes[0]); // stdinを閉じる

// stdoutの内容を全て読み込む
$StdOut = '';
while(!feof($pipes[1])) {
$StdOut .= fgets($pipes[1], 1024);
}
fclose($pipes[1]); // stdoutを閉じる

// stderrの内容を全て読み込む
while(!feof($pipes[2])) {
$StdOut .= fgets($pipes[2], 1024);
}
fclose($pipes[2]); // stderrを閉じる

// プロセスを終了する
proc_close($process);

return $StdOut;
}

Webフォームでファイルのアップロードを行い、そのファイルをGnuPGで暗号化する関数。

Apacheの設定によってはシェル環境が引き継がれていないため、gpgコマンドを呼び出すときに、ユーザのホームディレクトリを指定しないとエラーになる場合がある。(--homedir スイッチで強制的にディレクトリを指定)
また、単純に公開鍵をインポートしただけでは、鍵の信頼性が設定されていないため、暗号化時にチェックに引っかかり暗号化出来ない場合がある。全ての鍵の信頼性チェックを無効化するため、 --trust-model always スイッチを付加する。


function upload_and_encrypt($FILES, $sKey)
{
$strReturn = ''; // 実行結果、エラー等を返す変数

$uploaddir = './upload/';
$uploadfile = $uploaddir . basename($FILES['userfile']['name']);

if(!file_exists($FILES['userfile']['tmp_name']))
{
$strReturn .= "ERROR:ファイルのアップロードに失敗しました\n";
return $strReturn;
}
if (!move_uploaded_file($FILES['userfile']['tmp_name'], $uploadfile))
{
$strReturn .= "ERROR:アップロードファイルがTMPエリアから移動できません\n";
unlink($FILES['userfile']['tmp_name']); // 可能であればファイル削除
return $strReturn;
}

// シェルに引き渡すべきでない文字をエスケープする
$strInputfile = escapeshellcmd($uploadfile);
$sKey = escapeshellcmd($sKey);

$strReturn .= "対象ファイル : ".$strInputfile."\n";
$strReturn .= "暗号化に用いる公開鍵 : ".$sKey."\n";

$cmd = "/usr/local/bin/gpg --homedir '/home/tmg1136-inue2/.gnupg' --trust-model always --textmode -vv -e -a -r ".$sKey." ".$strInputfile;

// GnuPGを起動する
$strResult = exec_gpg($cmd);

// 元ファイルの削除
unlink($strInputfile);

return $strReturn;
}

暗号化されたファイルをダウンロードさせる、もしくは画面表示するための関数


function download_file($filename, $mode=0)
{
$strReturn = '';

// 暗号化ファイルは、.ascの拡張子を付ける
$uploaddir = './upload/';
$filename = $uploaddir . basename($filename) . '.asc';

if(!file_exists($filename))
{
$strReturn .= "ERROR:暗号化に失敗しています(ファイル出力されず)\n";
return $strReturn;
}

$fi = fopen($filename, "r");
if($fi == null)
{
$strReturn = "ERROR:暗号化ファイルは存在しますが、開くことが出来ません\n";
unlink($filename); // 可能であればファイルを削除しておく
return $strReturn;
}

$strContents = '';
while(!feof($fi)) {
$strContents .= fgets($fi, 1024);
}

fclose($fi);

if($mode == 0)
{ // 画面表示
print("\t<pre>\n".htmlspecialchars($strContents)."</pre>\n");
}
else
{ // ファイルのダウンロード
header('Content-Disposition: attachment; filename="'.basename($filename).'"');
// IE 対策 http://support.microsoft.com/default.aspx?scid=kb;ja;436605
//header('Content-Disposition: inline; filename="'.$filename.'"');
header('Content-Type: application/octet-stream');
header('Content-Transfer-Encoding: binary');
header('Content-Length: '.filesize($filename));
echo $strContents;

unlink($filename);
}

return $strReturn;
}

共通部分と、Webフォーム表示関数


<?php
if(isset($_GET['download']))
{ // 暗号化したファイルをダウンロードさせる場合の処理
$uploaddir = './upload/';
$filename = $uploaddir . basename($_GET['download']) . '.asc';
if(file_exists($filename))
{ // ファイルが存在する場合のみ、ダウンロード処理を開始
download_file($_GET['download'], 1);
exit();
}
}
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Language" content="ja" />
<?php
if(!empty($_FILES['userfile']['name']) && isset($_POST['key']) && !empty($_POST['key']))
{ // 指定秒数経過後に、ダウンロード処理のURLへ自動転送するためのMETAタグ挿入
$_downloadfilename = $_FILES['userfile']['name'];
print("\t<meta http-equiv=\"Refresh\" content=\"5; URL=https://www.example.com/index.php?download=$_downloadfilename\" />");
}
?>
<title> </title>
</head>
<body>
<?php

// 言語と文字コードの設定
mb_language('Japanese');
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');

// このページのファイル名(リロード用)
$strReloadPage = 'index.php';

print("<p>GPG-Webフロントエンド</p>\n");


if(!empty($_FILES['userfile']['name']) && isset($_POST['key']) && !empty($_POST['key']))
{ // ファイルがアップロードと暗号化
$strResult = upload_and_encrypt($_FILES, $_POST['key']);

// 暗号化のサマリーを表示
print("
<pre>\n".htmlspecialchars($strResult)."</pre>
");

download_file($_FILES['userfile']['name']); // エンコード結果を画面表示

}
else
{ // メニューページ(入力フォーム)を表示する
display_form($strReloadPage);
}


exit();


function display_form($strReloadPage)
{
// ファイル アップロードのダイアログを表示する
print("
<p>ファイルを暗号化する</p>
<form enctype=\"multipart/form-data\" action=\"$strReloadPage\" method=\"POST\">
<table border=\"0\" cellpadding=\"2\" cellspacing=\"0\" align=\"center\" width=\"700px\">
<tr><td>公開鍵</td><td><input name=\"key\" size=\"40\" /></td></tr>
<!-- アップロードするファイルのサイズ上限設定 -->
<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"30000\" />
<tr><td>対象ファイル</td><td><input name=\"userfile\" type=\"file\" size=\"40\" /></td></tr>
<tr><td></td><td><input type=\"submit\" value=\"暗号化実行\" /></td></tr>
</table>
</form>
");

return;
}