UNPKG

expo-crypto

Version:

Provides cryptography primitives for Android, iOS and web.

166 lines 6.44 kB
import { registerWebModule, NativeModule } from 'expo'; import { AESKeySize } from './aes.types'; import { base64ToUintArray, binaryInputBytes, bytesToHex, hexToUintArray, uint8ArrayToBase64, } from './web-utils'; const DEFAULT_IV_LENGTH = 12; const DEFAULT_TAG_LENGTH = 16; const defaultConfig = { ivLength: DEFAULT_IV_LENGTH, tagLength: DEFAULT_TAG_LENGTH, }; class EncryptionKey { key; keySize; constructor(key, size) { this.key = key; this.keySize = size; } static async generate(size) { const keySize = size ?? AESKeySize.AES256; const algorithm = { name: 'AES-GCM', length: keySize }; const key = await crypto.subtle.generateKey(algorithm, true, ['encrypt', 'decrypt']); return new EncryptionKey(key, keySize); } static async import(input, encoding) { let bytes; if (typeof input === 'string') { bytes = encoding === 'base64' ? base64ToUintArray(input) : hexToUintArray(input); } else { bytes = input; } const key = await crypto.subtle.importKey('raw', bytes, 'AES-GCM', true, [ 'encrypt', 'decrypt', ]); return new EncryptionKey(key, bytes.byteLength * 8); } async bytes() { const buffer = await crypto.subtle.exportKey('raw', this.key); return new Uint8Array(buffer); } async encoded(encoding) { const bytes = await this.bytes(); const encoded = encoding === 'base64' ? uint8ArrayToBase64(bytes) : bytesToHex(bytes); return encoded; } get size() { return this.keySize; } } class SealedData { buffer; config; constructor(buffer, config) { this.buffer = buffer; this.config = config; } static fromCombined(combined, config) { const buffer = binaryInputBytes(combined).buffer; return new SealedData(buffer, config ?? defaultConfig); } static fromParts(iv, ciphertext, tag) { const ciphertextBytes = binaryInputBytes(ciphertext); const ivBytes = binaryInputBytes(iv); const ivLength = ivBytes.byteLength; if (!tag) { tag = DEFAULT_TAG_LENGTH; } if (typeof tag === 'number') { const totalLength = ivLength + ciphertextBytes.byteLength; const combined = new Uint8Array(totalLength); combined.set(ivBytes); combined.set(ciphertextBytes, ivLength); const config = { ivLength, tagLength: tag, }; return new SealedData(combined.buffer, config); } const tagBytes = binaryInputBytes(tag); const tagLength = tagBytes.byteLength; const totalLength = ivLength + ciphertextBytes.byteLength + tagLength; const combined = new Uint8Array(totalLength); combined.set(ivBytes); combined.set(ciphertextBytes, ivLength); combined.set(tagBytes, totalLength - tagLength); return new SealedData(combined.buffer, { ivLength, tagLength }); } get ivSize() { return this.config.ivLength; } get tagSize() { return this.config.tagLength; } get combinedSize() { return this.buffer.byteLength; } async iv(encoding) { const useBase64 = encoding === 'base64'; const bytes = new Uint8Array(this.buffer, 0, this.ivSize); return useBase64 ? uint8ArrayToBase64(bytes) : bytes; } async tag(encoding) { const useBase64 = encoding === 'base64'; const offset = this.combinedSize - this.tagSize; const bytes = new Uint8Array(this.buffer, offset, this.tagSize); return useBase64 ? uint8ArrayToBase64(bytes) : bytes; } async combined(encoding) { const useBase64 = encoding === 'base64'; const bytes = new Uint8Array(this.buffer); return useBase64 ? uint8ArrayToBase64(bytes) : bytes; } async ciphertext(options) { const includeTag = options?.includeTag ?? false; const useBase64 = options?.encoding === 'base64'; const taggedCiphertextLength = this.combinedSize - this.ivSize; const ciphertextLength = includeTag ? taggedCiphertextLength : taggedCiphertextLength - this.tagSize; const bytes = new Uint8Array(this.buffer, this.ivSize, ciphertextLength); return useBase64 ? uint8ArrayToBase64(bytes) : bytes; } } class AesCryptoModule extends NativeModule { EncryptionKey = EncryptionKey; SealedData = SealedData; async encryptAsync(plaintext, key, options = {}) { const { nonce = DEFAULT_IV_LENGTH, tagLength = DEFAULT_TAG_LENGTH, additionalData: aad, } = options; const iv = typeof nonce === 'number' ? crypto.getRandomValues(new Uint8Array(nonce)) : binaryInputBytes(nonce); const baseParams = { name: 'AES-GCM', iv, tagLength: tagLength * 8 }; // workaround for invalid AAD format error when it's present but undefined const gcmParams = aad ? { ...baseParams, additionalData: binaryInputBytes(aad), } : baseParams; const ciphertextWithTag = await crypto.subtle.encrypt(gcmParams, key.key, binaryInputBytes(plaintext)); return SealedData.fromParts(iv, ciphertextWithTag, tagLength); } async decryptAsync(sealedData, key, options = {}) { const { additionalData: aad, output } = options; // workaround for invalid AAD format error when it's present but undefined const iv = await sealedData.iv(); const baseParams = { name: 'AES-GCM', iv: iv, tagLength: sealedData.tagSize * 8, }; const gcmParams = aad ? { ...baseParams, additionalData: binaryInputBytes(aad), } : baseParams; const taggedCiphertext = await sealedData.ciphertext({ includeTag: true }); const plaintextBuffer = await crypto.subtle.decrypt(gcmParams, key.key, taggedCiphertext); const useBase64 = output === 'base64'; const bytes = new Uint8Array(plaintextBuffer); return useBase64 ? uint8ArrayToBase64(bytes) : bytes; } } export default registerWebModule(AesCryptoModule, 'ExpoCryptoAES'); //# sourceMappingURL=ExpoCryptoAES.web.js.map