PHP Conference Japan 2024

簡介

Sodium 是一個現代、易於使用的軟體函式庫,用於加密、解密、簽名、密碼雜湊等。其目標是提供建構更高層級加密工具所需的所有核心操作。

新增註解

使用者貢獻註解 6 則註解

64
rafayhingoro at hotmail dot com
6 年前
<?php
//簡單用法

/**
* 加密訊息
*
* @param string $message - 要加密的訊息
* @param string $key - 加密金鑰
* @return string
*/
function safeEncrypt($message, $key)
{
$nonce = random_bytes(
SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
);

$cipher = base64_encode(
$nonce.
sodium_crypto_secretbox(
$message,
$nonce,
$key
)
);
sodium_memzero($message);
sodium_memzero($key);
return
$cipher;
}

/**
* 解密訊息
*
* @param string $encrypted - 使用 safeEncrypt() 加密的訊息
* @param string $key - 加密金鑰
* @return string
*/
function safeDecrypt($encrypted, $key)
{
$decoded = base64_decode($encrypted);
if (
$decoded === false) {
throw new
Exception('發生嚴重的編碼錯誤');
}
if (
mb_strlen($decoded, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + SODIUM_CRYPTO_SECRETBOX_MACBYTES)) {
throw new
Exception('發生嚴重的訊息截斷錯誤');
}
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');

$plain = sodium_crypto_secretbox_open(
$ciphertext,
$nonce,
$key
);
if (
$plain === false) {
throw new
Exception('訊息在傳輸過程中遭到竄改');
}
sodium_memzero($ciphertext);
sodium_memzero($key);
return
$plain;
}
//加密和解密您的訊息
$key = sodium_crypto_secretbox_keygen();
$enc = safeEncrypt('Abdul Rafay Hingoro', $key); //產生隨機加密字串 (Base64 相關)
echo $enc;
echo
'<br>';
$dec = safeDecrypt($enc, $key); //解密透過 safeEncrypt 函式產生的編碼字串
echo $dec;
?>

//輸出
DEx9ATXEg/eRq8GWD3NT5BatB3m31WEDEYLK2V4L0Am5GZGoa2rvYWUpoUeCrm7W/pdgLJrNoE6AA8U=
Abdul Rafay Hingoro
8
匿名
6 年前
顯然目前對此沒有太多支援或文件。

根據此處和其他地方相同的 safeEncrypt 實作,我更新它以使其對我有效(我正在執行 libsodium 1.0.8)。

我將這些方法新增至一個工具類別。

<?php

// 簡單範例
$message = '我的極機密資訊';

$secret_key = Util::generateSecretKey();
$encrypted = Util::encrypt($message, $secret_key, 64);
$decrypted = Util::decrypt($encrypted, $secret_key, 64);

print
$decrypted;

// 輸出 '我的極機密資訊'
// 加密資料會以 64 位元組區塊進行填充,以混淆實際資料長度

class Util
{
/**
* 取得用於加密/解密的密鑰
*
* 使用 libsodium 產生密鑰。應妥善保管此密鑰。
*
* @return string
* @see encrypt(), decrypt()
*/
public static function generateSecretKey()
{
return
sodium_crypto_secretbox_keygen();
}

/**
* 加密訊息
*
* 使用 libsodium 加密字串
*
* @param string $message - 要加密的訊息
* @param string $secret_key - 加密金鑰
* @param int $block_size - 將訊息以 $block_size 位元組區塊填充,以隱藏加密資料的大小。加密與解密時必須一致!
* @return string
* @see decrypt()
* @see https://github.com/jedisct1/libsodium/issues/392
*/
public static function encrypt($message, $secret_key, $block_size = 1)
{
// 為此操作建立一個隨機數 (nonce)。它將儲存在訊息本身中並在之後還原。
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);

// 填充為 $block_size 位元組的區塊 (強制限制為 512 位元組)
$padded_message = sodium_pad($message, $block_size <= 512 ? $block_size : 512);

// 加密訊息並與隨機數合併
$cipher = base64_encode($nonce . sodium_crypto_secretbox($padded_message, $nonce, $secret_key));

// 清理
sodium_memzero($message);
sodium_memzero($secret_key);

return
$cipher;
}

/**
* 解密訊息
*
* 使用 libsodium 解密已加密的字串
*
* @param string $encrypted - 已加密的訊息
* @param string $key - 加密金鑰
* @param int $block_size - 將訊息以 $block_size 位元組區塊填充,以隱藏加密資料的大小。加密與解密時必須一致!
* @return string
* @see encrypt()
* @see https://github.com/jedisct1/libsodium/issues/392
*/
public static function decrypt($encrypted, $secret_key, $block_size = 1)
{
// 解開 base64 訊息
$decoded = base64_decode($encrypted);

// 檢查一般性失敗
if ($decoded === false) {
throw new
\Exception('編碼失敗');
}

// 檢查訊息是否不完整。此版本中似乎不存在 CRYPTO_SECRETBOX_MACBYTES...
if (!defined('CRYPTO_SECRETBOX_MACBYTES')) define('CRYPTO_SECRETBOX_MACBYTES', 16);
if (
mb_strlen($decoded, '8bit') < (SODIUM_CRYPTO_SECRETBOX_NONCEBYTES + CRYPTO_SECRETBOX_MACBYTES)) {
throw new
\Exception('訊息被截斷');
}

// 從解開的訊息中提取隨機數 (nonce) 和密文
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');

// 解密並考慮來自 $block_size 的額外填充 (強制限制為 512 位元組)
$decrypted_padded_message = sodium_crypto_secretbox_open($ciphertext, $nonce, $secret_key);
$message = sodium_unpad($decrypted_padded_message, $block_size <= 512 ? $block_size : 512);

// 檢查解密失敗
if ($message === false) {
throw new
\Exception('訊息在傳輸過程中遭到竄改');
}

// 清理
sodium_memzero($ciphertext);
sodium_memzero($secret_key);

return
$message;
}

}

?>
2
jedisct1 at php dot net
6 年前
對於敏感資料,請使用 `sodium_bin2base64()` 和 `sodium_base642bin()` 而非 `base64_encode()` 和 `base64_decode()`。

sodium 函數能提供更好的防禦側通道攻擊保護、更具彈性,並執行更嚴格的驗證。
2
jedisct1 at php dot net
6 年前
## 使用密鑰加密單一訊息

加密

```php
$secret_key = sodium_crypto_secretbox_keygen();
$message = '敏感資訊';

$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$encrypted_message = sodium_crypto_secretbox($message, $nonce, $secret_key);
```

解密

```php
$decrypted_message = sodium_crypto_secretbox_open($encrypted_message, $nonce, $secret_key);
```

運作方式

`$secret_key` 是一個密鑰。不是密碼。它是二進位資料,不是
設計給人類閱讀的內容,而是為了在給定的長度內擁有盡可能大的密鑰
空間。
`keygen()` 函數會建立這樣的密鑰。它必須保持機密,
因為它同時用於加密和解密資料。

`$nonce` 是一個唯一值。與密鑰一樣,其長度是固定的。但
它不一定要保密,並且可以與加密
訊息一起傳送。隨機數也不一定要不可預測。它只是對於給定的密鑰必須是
唯一的。使用 `secretbox()` API,使用
`random_bytes()` 是產生隨機數的完全可行的方式。

加密訊息會比未加密訊息略大,
因為它們包含一個驗證器,解密函數
會使用該驗證器來檢查內容是否被竄改。

## 使用密鑰加密單一訊息,並隱藏其長度

加密

```php
$secret_key = sodium_crypto_secretbox_keygen();
$message = '敏感資訊';
$block_size = 16;

$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$padded_message = sodium_pad($padded_message, $block_size);
$encrypted_message = sodium_crypto_secretbox($padded_message, $nonce, $secret_key);
```

解密

```php
$decrypted_padded_message = sodium_crypto_secretbox_open($encrypted_message, $nonce, $secret_key);
$decrypted_message = sodium_unpad($decrypted_padded_message, $block_size);
```

運作方式

有時,訊息的長度可能會提供許多有關
其性質的資訊。如果訊息是「是」、「否」和「可能」之一,
加密訊息沒有幫助:知道長度就足以
知道訊息是什麼。

填充是一種透過將長度設定為
給定區塊大小的倍數來減輕此問題的技術。

訊息必須在加密之前進行填充,並在
解密後進行移除填充。
1
jedisct1 at php dot net
6 年前
## 使用密鑰加密檔案

<?php
$secret_key
= sodium_crypto_secretstream_xchacha20poly1305_keygen();
$input_file = '/tmp/example.original';
$encrypted_file = '/tmp/example.enc';
$chunk_size = 4096;

$fd_in = fopen($input_file, 'rb');
$fd_out = fopen($encrypted_file, 'wb');

list(
$stream, $header) = sodium_crypto_secretstream_xchacha20poly1305_init_push($secret_key);

fwrite($fd_out, $header);

$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
do {
$chunk = fread($fd_in, $chunk_size);
if (
stream_get_meta_data($fd_in)['unread_bytes'] <= 0) {
$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL;
}
$encrypted_chunk = sodium_crypto_secretstream_xchacha20poly1305_push($stream, $chunk, '', $tag);
fwrite($fd_out, $encrypted_chunk);
} while (
$tag !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL);

fclose($fd_out);
fclose($fd_in);
?>

解密檔案

<?php
$decrypted_file
= '/tmp/example.dec';

$fd_in = fopen($encrypted_file, 'rb');
$fd_out = fopen($decrypted_file, 'wb');

$header = fread($fd_in, SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_HEADERBYTES);

$stream = sodium_crypto_secretstream_xchacha20poly1305_init_pull($header, $secret_key);

$tag = SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_MESSAGE;
while (
stream_get_meta_data($fd_in)['unread_bytes'] > 0 &&
$tag !== SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_TAG_FINAL) {
$chunk = fread($fd_in, $chunk_size + SODIUM_CRYPTO_SECRETSTREAM_XCHACHA20POLY1305_ABYTES);
list(
$decrypted_chunk, $tag) = sodium_crypto_secretstream_xchacha20poly1305_pull($stream, $chunk);
fwrite($fd_out, $decrypted_chunk);
}
$ok = stream_get_meta_data($fd_in)['unread_bytes'] <= 0;

fclose($fd_out);
fclose($fd_in);

if (!
$ok) {
die(
'Invalid/corrupted input');
}
?>

運作方式

這裡的程式碼比之前的範例多一點。

事實上,`crypto_secretbox()` 也可用於加密檔案,但
前提是該檔案必須很小。由於我們必須將整個
內容以字串形式提供,因此必須能放入記憶體。

如果檔案很大,我們可以將其分割成小區塊,並個別加密
區塊。

透過這樣做,我們可以加密任意大小的檔案。但我們需要確保
區塊不會被刪除、截斷、複製和
重新排序。換句話說,我們沒有單一的「訊息」,而是一個
訊息流,在解密過程中,我們需要一種方法
來檢查整個訊息流是否與我們加密的內容相符。

因此,我們建立一個新的訊息流 (`init_push`) 並將一系列訊息
推入其中 (`push`)。每個單獨的訊息都有一個標籤附加到它上面,預設是 `TAG_MESSAGE`。為了讓解密過程知道
訊息流的結尾在哪裡,我們用 `TAG_FINAL` 標籤標記最後一個訊息。
訊息流的結尾在哪裡,我們用
`TAG_FINAL` 標籤標記最後一個訊息。

當我們消耗訊息流(`init_pull`,然後對每個
訊息使用 `pull`)時,我們會檢查它們是否可以正確解密,並檢索
解密的區塊和附加的標籤。如果我們讀取最後一個
區塊 (`TAG_FINAL`) 並且我們在檔案的末尾,我們知道我們
已完整還原原始訊息流。
2
jedisct1 at php dot net
6 年前
你可能會想在上面的範例中使用 `feof($fd_in)` 來取代 `stream_get_meta_data($fd_in)['unread_bytes'] <= 0`。

或者直接閱讀這裡的 PHP 擴充功能的說明文件:https://github.com/jedisct1/libsodium-php -- 它總是最新版本。
To Top