轻松加密打字稿
#教程 #安全 #typescript #node

tl; dr:nodejs crypto应提供对 ChaCha20-Poly1305的访问在可预见的将来足够强大。

密码学很难。 密码学甚至更难。值得庆幸的是,大多数现代后端服务都为现代开发人员和Nodejs变得更加容易提供了许多手段。

当我开始潜入nodejs时,试图在nodejs中找到良好的加密指南是 hard 。有几个博客,一些帖子,但没有什么具体的,没有什么可以提供有关某些方法比其他方法更好的答案。因此,我决定阅读一些有关密码学的书籍,并阅读一些Nodejs文档。

基本加密使用nodejs

nodejs在其crypto模块中提供了一些有用的加密实用程序,它允许开发人员使用本地安装的OpenSSL模块中可用的任何算法和方法,如果已安装,则可以在大多数系统上使用以下命令找到,

openssl list -cipher-algorithms

如果安装了OpenSSL,则应为您提供像这样的列表:

AES-128-CBC
AES-128-CBC-HMAC-SHA1
AES-128-CBC-HMAC-SHA256
AES-128-CFB
AES-128-CFB1
AES-128-CFB8
AES-128-CTR
AES-128-ECB
AES-128-OCB
AES-128-OFB
AES-128-XTS
AES-192-CBC
AES-192-CFB
AES-192-CFB1
AES-192-CFB8
AES-192-CTR
AES-192-ECB
AES-192-OCB
AES-192-OFB
AES-256-CBC
AES-256-CBC-HMAC-SHA1
AES-256-CBC-HMAC-SHA256
. . .

对于大多数人而言,不需要任何严重的加密安全性,而只是寻求通过加密某些值来确保应用程序的应用程序,aes-256-cbc就足够了,对于消息身份验证的某些外观,使用aes-256-cbc-hmac-sha256可以正常工作。因此,让我们看看Typescript中的外观。

AES-256-CBC

import * as crypto from 'crypto';

function splitEncryptedText( encryptedText: string ) {
    return {
        ivString: encryptedText.slice( 0, 32 ),
        encryptedDataString: encryptedText.slice( 32 ),
    }
}

export default class Security {
    encoding: BufferEncoding = 'hex';

    // process.env.CRYPTO_KEY should be a 32 BYTE key
    key: string = process.env.CRYPTO_KEY;

    encrypt( plaintext: string ) {
        try {
            const iv = crypto.randomBytes( 16 );
            const cipher = crypto.createCipheriv( 'aes-256-cbc', this.key, iv );

            const encrypted = Buffer.concat( [
                cipher.update(
                    plaintext, 'utf-8'
                ),
                cipher.final(),
            ] );

            return iv.toString( this.encoding ) + encrypted.toString( this.encoding );

        } catch (e) {
            console.error( e );
        }
    };

    decrypt( cipherText: string ) {
        const {
            encryptedDataString,
            ivString,
        } = splitEncryptedText( cipherText );

        try {
            const iv = Buffer.from( ivString, this.encoding );
            const encryptedText = Buffer.from( encryptedDataString, this.encoding );

            const decipher = crypto.createDecipheriv( 'aes-256-cbc', this.key, iv );

            const decrypted = decipher.update( encryptedText );
            return Buffer.concat( [ decrypted, decipher.final() ] ).toString();
        } catch (e) {
            console.error( e );
        }
    }
}

让我们遍历这里发生的事情。

首先,在encrypt方法内,我们实例化了iv初始化向量,它用作链中XOR方法的第一个来源(CBC代表密码加密数据的密码块链)。这应该始终是随机的,最好在密码上安全,但不需要秘密。

const iv = crypto.randomBytes( 16 );

然后,我们创建加密密码,指定算法,键和IV。

接下来,我们需要使用cipher.update()方法对文本进行加密,然后调用cipher.final()关闭密码并确保无法进行其他更改(与密码相互作用的任何进一步尝试会导致丢弃错误)。

之后,我们将IV和加密的字符串组合在一起,将它们都转换为指定的编码,我使用了hex编码,但这并不重要,只要解密时使用了相同的编码。

说到解密,这只是加密的相反。我们将提供的密文将所提供的密文拆分为加密的文本和IV,将IV与Decipher一起使用并恢复原始文本。

正如我所说,这非常简单,将在大多数情况下工作。现在,应该提到的是,AES-CBC 使用上述aes-256-cbc-hmac-sha256可能容易受到Padding Oracle Attack,的影响,并且与允许进行身份验证的消息中的下一个方法最相似,这意味着任何尝试的尝试干扰不应工作。

chacha20-poly1305

现在,对于主要事件,一种更健壮,现代且可以说是更安全的加密方法。应该注意的是,这两种方法并非严格可比性,普通的CBC并未对消息进行身份验证,因此与ChaCha20-Poly1305有很大不同。话虽如此,实施它非常简单,只需要从上面的示例中进行几行更改。

import * as crypto from 'crypto';

function splitEncryptedText( encryptedText: string ) {
    return {
        encryptedDataString: encryptedText.slice( 56, -32 ),
        ivString: encryptedText.slice( 0, 24 ),
        assocDataString: encryptedText.slice( 24, 56 ),
        tagString: encryptedText.slice( -32 ),
    }
}

export default class Security {
    encoding: BufferEncoding = 'hex';

    // process.env.CRYPTO_KEY should be a 32 BYTE key
    key: string = process.env.CRYPTO_KEY;

    encrypt( plaintext: string ) {
        try {
            const iv = crypto.randomBytes( 12 );
            const assocData = crypto.randomBytes( 16 );
            const cipher = crypto.createCipheriv( 'chacha20-poly1305', this.key, iv, {
                authTagLength: 16,
            } );

            cipher.setAAD( assocData, { plaintextLength: Buffer.byteLength( plaintext ) } );

            const encrypted = Buffer.concat( [
                cipher.update(
                    plaintext, 'utf-8'
                ),
                cipher.final(),
            ] );
            const tag = cipher.getAuthTag();

            return iv.toString( this.encoding ) + assocData.toString( this.encoding ) + encrypted.toString( this.encoding ) + tag.toString( this.encoding );

        } catch (e) {
            console.error( e );
        }
    };

    decrypt( cipherText: string ) {
        const {
            encryptedDataString,
            ivString,
            assocDataString,
            tagString,
        } = splitEncryptedText( cipherText );

        try {
            const iv = Buffer.from( ivString, this.encoding );
            const encryptedText = Buffer.from( encryptedDataString, this.encoding );
            const tag = Buffer.from( tagString, this.encoding );

            const decipher = crypto.createDecipheriv( 'chacha20-poly1305', this.key, iv, { authTagLength: 16 } );
            decipher.setAAD( Buffer.from( assocDataString, this.encoding ), { plaintextLength: encryptedDataString.length } );
            decipher.setAuthTag( Buffer.from( tag ) );

            const decrypted = decipher.update( encryptedText );
            return Buffer.concat( [ decrypted, decipher.final() ] ).toString();
        } catch (e) {
            console.error( e );
        }
    }
}

CHACHA20-POLY1305的潜在问题

您可能会问,即使在hmac-sha256的情况下,Chacha20-Poly1305是否比aes-256-cbc好得多,为什么还要提及它们呢?好吧,这归结为简单的东西。 IV(nonce)大小,96位。

虽然这通常不是问题,但它 小于aes-cbc方法,这意味着它在这方面不太安全。有一个修复程序,XChaCha20-Poly1305引入了192位IV(NONCE)。不幸的是,到目前为止(30/04/2022),这还没有进入OpenSSL,因此还没有进入Nodejs Crypto模块。因此,就目前而言,我们一直坚持使用标准ChaCha20-Poly1305,而无需尝试自己实施。


rc.xyz NFT gallery的标题Unsplash