feat: 新增共享加密包并引入 ZIP/PGP 依赖
This commit is contained in:
53
packages/crypto/src/aes-gcm.ts
Normal file
53
packages/crypto/src/aes-gcm.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto'
|
||||
|
||||
const GCM_IV_LENGTH = 12 // 96 bits
|
||||
const GCM_TAG_LENGTH = 16 // 128 bits
|
||||
const ALGORITHM = 'aes-256-gcm'
|
||||
|
||||
/**
|
||||
* AES-256-GCM encrypt.
|
||||
*
|
||||
* Output format (before Base64): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)]
|
||||
*
|
||||
* @param plaintext - UTF-8 string to encrypt
|
||||
* @param key - 32-byte AES key
|
||||
* @returns Base64-encoded encrypted data
|
||||
*/
|
||||
export const aesGcmEncrypt = (plaintext: string, key: Buffer): string => {
|
||||
const iv = randomBytes(GCM_IV_LENGTH)
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH })
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()])
|
||||
const tag = cipher.getAuthTag()
|
||||
|
||||
// Layout: IV + ciphertext + tag
|
||||
const combined = Buffer.concat([iv, encrypted, tag])
|
||||
return combined.toString('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* AES-256-GCM decrypt.
|
||||
*
|
||||
* Input format (after Base64 decode): [IV (12 bytes)] + [ciphertext] + [auth tag (16 bytes)]
|
||||
*
|
||||
* @param encryptedBase64 - Base64-encoded encrypted data
|
||||
* @param key - 32-byte AES key
|
||||
* @returns Decrypted UTF-8 string
|
||||
*/
|
||||
export const aesGcmDecrypt = (encryptedBase64: string, key: Buffer): string => {
|
||||
const data = Buffer.from(encryptedBase64, 'base64')
|
||||
|
||||
if (data.length < GCM_IV_LENGTH + GCM_TAG_LENGTH) {
|
||||
throw new Error('Encrypted data too short: must contain IV + tag at minimum')
|
||||
}
|
||||
|
||||
const iv = data.subarray(0, GCM_IV_LENGTH)
|
||||
const tag = data.subarray(data.length - GCM_TAG_LENGTH)
|
||||
const ciphertext = data.subarray(GCM_IV_LENGTH, data.length - GCM_TAG_LENGTH)
|
||||
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: GCM_TAG_LENGTH })
|
||||
decipher.setAuthTag(tag)
|
||||
|
||||
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
||||
return decrypted.toString('utf-8')
|
||||
}
|
||||
15
packages/crypto/src/hash.ts
Normal file
15
packages/crypto/src/hash.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* Compute SHA-256 hash and return raw Buffer.
|
||||
*/
|
||||
export const sha256 = (data: string | Buffer): Buffer => {
|
||||
return createHash('sha256').update(data).digest()
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute SHA-256 hash and return lowercase hex string.
|
||||
*/
|
||||
export const sha256Hex = (data: string | Buffer): string => {
|
||||
return createHash('sha256').update(data).digest('hex')
|
||||
}
|
||||
15
packages/crypto/src/hkdf.ts
Normal file
15
packages/crypto/src/hkdf.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { hkdfSync } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* Derive a key using HKDF-SHA256.
|
||||
*
|
||||
* @param ikm - Input keying material (string, will be UTF-8 encoded)
|
||||
* @param salt - Salt value (string, will be UTF-8 encoded)
|
||||
* @param info - Info/context string (will be UTF-8 encoded)
|
||||
* @param length - Output key length in bytes (default: 32 for AES-256)
|
||||
* @returns Derived key as Buffer
|
||||
*/
|
||||
export const hkdfSha256 = (ikm: string, salt: string, info: string, length = 32): Buffer => {
|
||||
const derived = hkdfSync('sha256', ikm, salt, info, length)
|
||||
return Buffer.from(derived)
|
||||
}
|
||||
23
packages/crypto/src/hmac.ts
Normal file
23
packages/crypto/src/hmac.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createHmac } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* Compute HMAC-SHA256 and return Base64-encoded signature.
|
||||
*
|
||||
* @param key - HMAC key (Buffer)
|
||||
* @param data - Data to sign (UTF-8 string)
|
||||
* @returns Base64-encoded HMAC-SHA256 signature
|
||||
*/
|
||||
export const hmacSha256Base64 = (key: Buffer, data: string): string => {
|
||||
return createHmac('sha256', key).update(data, 'utf-8').digest('base64')
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute HMAC-SHA256 and return raw Buffer.
|
||||
*
|
||||
* @param key - HMAC key (Buffer)
|
||||
* @param data - Data to sign (UTF-8 string)
|
||||
* @returns HMAC-SHA256 digest as Buffer
|
||||
*/
|
||||
export const hmacSha256 = (key: Buffer, data: string): Buffer => {
|
||||
return createHmac('sha256', key).update(data, 'utf-8').digest()
|
||||
}
|
||||
6
packages/crypto/src/index.ts
Normal file
6
packages/crypto/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { aesGcmDecrypt, aesGcmEncrypt } from './aes-gcm'
|
||||
export { sha256, sha256Hex } from './hash'
|
||||
export { hkdfSha256 } from './hkdf'
|
||||
export { hmacSha256, hmacSha256Base64 } from './hmac'
|
||||
export { generatePgpKeyPair, pgpSignDetached, pgpVerifyDetached } from './pgp'
|
||||
export { rsaOaepEncrypt } from './rsa-oaep'
|
||||
75
packages/crypto/src/pgp.ts
Normal file
75
packages/crypto/src/pgp.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as openpgp from 'openpgp'
|
||||
|
||||
/**
|
||||
* Generate an OpenPGP RSA key pair.
|
||||
*
|
||||
* @param name - User name for the key
|
||||
* @param email - User email for the key
|
||||
* @returns ASCII-armored private and public keys
|
||||
*/
|
||||
export const generatePgpKeyPair = async (
|
||||
name: string,
|
||||
email: string,
|
||||
): Promise<{ privateKey: string; publicKey: string }> => {
|
||||
const { privateKey, publicKey } = await openpgp.generateKey({
|
||||
type: 'rsa',
|
||||
rsaBits: 2048,
|
||||
userIDs: [{ name, email }],
|
||||
format: 'armored',
|
||||
})
|
||||
|
||||
return { privateKey, publicKey }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached OpenPGP signature for the given data.
|
||||
*
|
||||
* @param data - Raw data to sign (Buffer or Uint8Array)
|
||||
* @param armoredPrivateKey - ASCII-armored private key
|
||||
* @returns ASCII-armored detached signature (signature.asc content)
|
||||
*/
|
||||
export const pgpSignDetached = async (data: Uint8Array, armoredPrivateKey: string): Promise<string> => {
|
||||
const privateKey = await openpgp.readPrivateKey({ armoredKey: armoredPrivateKey })
|
||||
const message = await openpgp.createMessage({ binary: data })
|
||||
|
||||
const signature = await openpgp.sign({
|
||||
message,
|
||||
signingKeys: privateKey,
|
||||
detached: true,
|
||||
format: 'armored',
|
||||
})
|
||||
|
||||
return signature as string
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a detached OpenPGP signature.
|
||||
*
|
||||
* @param data - Original data (Buffer or Uint8Array)
|
||||
* @param armoredSignature - ASCII-armored detached signature
|
||||
* @param armoredPublicKey - ASCII-armored public key
|
||||
* @returns true if signature is valid
|
||||
*/
|
||||
export const pgpVerifyDetached = async (
|
||||
data: Uint8Array,
|
||||
armoredSignature: string,
|
||||
armoredPublicKey: string,
|
||||
): Promise<boolean> => {
|
||||
const publicKey = await openpgp.readKey({ armoredKey: armoredPublicKey })
|
||||
const signature = await openpgp.readSignature({ armoredSignature })
|
||||
const message = await openpgp.createMessage({ binary: data })
|
||||
|
||||
const verificationResult = await openpgp.verify({
|
||||
message,
|
||||
signature,
|
||||
verificationKeys: publicKey,
|
||||
})
|
||||
|
||||
const { verified } = verificationResult.signatures[0]!
|
||||
try {
|
||||
await verified
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
34
packages/crypto/src/rsa-oaep.ts
Normal file
34
packages/crypto/src/rsa-oaep.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { constants, createPublicKey, publicEncrypt } from 'node:crypto'
|
||||
|
||||
/**
|
||||
* RSA-OAEP encrypt with platform public key.
|
||||
*
|
||||
* Algorithm: RSA/ECB/OAEPWithSHA-256AndMGF1Padding
|
||||
* - OAEP hash: SHA-256
|
||||
* - MGF1 hash: SHA-256
|
||||
*
|
||||
* @param plaintext - UTF-8 string to encrypt
|
||||
* @param publicKeyBase64 - Platform public key (X.509 DER, Base64 encoded)
|
||||
* @returns Base64-encoded ciphertext
|
||||
*/
|
||||
export const rsaOaepEncrypt = (plaintext: string, publicKeyBase64: string): string => {
|
||||
// Load public key from Base64-encoded DER (X.509 / SubjectPublicKeyInfo)
|
||||
const publicKeyDer = Buffer.from(publicKeyBase64, 'base64')
|
||||
const publicKey = createPublicKey({
|
||||
key: publicKeyDer,
|
||||
format: 'der',
|
||||
type: 'spki',
|
||||
})
|
||||
|
||||
// Encrypt with RSA-OAEP (SHA-256 for both OAEP hash and MGF1)
|
||||
const encrypted = publicEncrypt(
|
||||
{
|
||||
key: publicKey,
|
||||
padding: constants.RSA_PKCS1_OAEP_PADDING,
|
||||
oaepHash: 'sha256',
|
||||
},
|
||||
Buffer.from(plaintext, 'utf-8'),
|
||||
)
|
||||
|
||||
return encrypted.toString('base64')
|
||||
}
|
||||
Reference in New Issue
Block a user