UNPKG

@meeco/cryppo

Version:

In-browser encryption and decryption. Clone of Ruby Cryppo

200 lines 7.71 kB
import { BSON } from 'bson'; import { Buffer as _buffer } from 'buffer'; import forge from 'node-forge'; import { parse as yamlParse, stringify as yamlStringify } from 'yaml'; import { SerializationFormat } from './serialization-versions.js'; const { pki, random, util } = forge; // 65 is the version byte for encryption artefacts encoded with BSON const ENCRYPTION_ARTEFACTS_CURRENT_VERSION = 'A'; // 75 is the version byte for derivation artefacts encoded with BSON const DERIVATION_ARTEFACTS_CURRENT_VERSION = 'K'; /** * Wrapping some node-forge utils in case we ever need to replace it */ export const encode64 = util.encode64; export const decode64 = util.decode64; export const encodeUtf8 = util.encodeUtf8; export const utf8ToBytes = util.text.utf8.encode; export const utf16ToBytes = util.text.utf16.encode; export const binaryStringToBytes = util.binary.raw.decode; export const bytesToBinaryString = (bytes) => { let binary = ''; const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return binary; }; export const bytesToUtf16 = (bytes) => { let binary = ''; const utf16Bytes = new Uint16Array(bytes.buffer); const len = utf16Bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(utf16Bytes[i]); } return binary; }; export const bytesToUtf8 = (bytes) => { let binary = ''; const len = bytes.byteLength; for (let i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]); } return util.decodeUtf8(binary); }; export const binaryStringToBytesBuffer = (value) => _buffer.from(util.binary.raw.decode(value)); export const bytesBufferToBinaryString = (val) => // @ts-expect-error node-forge createBuffer accepts Uint8Array at runtime util.createBuffer(val).data; export const generateRandomBytesString = (length = 32) => random.getBytesSync(length); export function serializeDerivedKeyOptions(strategy, artifacts, serializationFormat = SerializationFormat.latest_version) { switch (serializationFormat) { case SerializationFormat.legacy: { const yaml = encodeYaml(artifacts); return `${strategy}.${encodeSafe64(yaml)}`; } default: { return `${strategy}.${encodeSafe64Bson(DERIVATION_ARTEFACTS_CURRENT_VERSION, artifacts)}`; } } } export function deSerializeDerivedKeyOptions(serialized) { let items = serialized.split('.'); // We might get passed an entire encrypted string in which case we just want the key and strategy if (items.length > 2) { items = items.slice(-2); } const [derivationStrategy, artifacts] = items; const serializationArtifacts = decodeArtifactData(artifacts); return { derivationStrategy, serializationArtifacts, }; } export function serialize(strategy, data, artifacts, serializationFormat = SerializationFormat.latest_version) { switch (serializationFormat) { case SerializationFormat.legacy: { const yaml = encodeYaml(artifacts); return `${strategy}.${encodeSafe64(data)}.${encodeSafe64(yaml)}`; } default: { return `${strategy}.${encodeSafe64(data)}.${encodeSafe64Bson(ENCRYPTION_ARTEFACTS_CURRENT_VERSION, artifacts)}`; } } } function encodeYaml(data) { // Note the pad and binary replacements are only for backwards compatibility // with Ruby Cryppo. They technically should not be required and there should // be a flag to disable them. const pad = `---\n`; return pad + yamlStringify(data, { schema: 'yaml-1.1' }).replace(/!!binary/g, '!binary'); } export function deSerialize(serialized) { const items = serialized.split('.'); if (items.length < 2) { throw new Error('String is not a serialized encrypted string'); } if (items.length % 2 !== 1) { throw new Error('Serialized string should have an encryption strategy and pairs of encoded data and artifacts'); } const [encryptionStrategy] = items; const decodedPairs = items.slice(1).map((item, i) => { if (i % 2 === 0) { // Base64 encoded encrypted data return decodeSafe64(item); } else { return decodeArtifactData(item); } }); if (!decodedPairs.length) { throw new Error('No data found to decrypt in serialized string'); } return { encryptionStrategy, decodedPairs, }; } function decodeArtifactData(text) { if (decodeSafe64(text).startsWith('---')) { text = decodeSafe64(text); return yamlParse(text.replace(/ !binary/g, ' !!binary'), { schema: 'yaml-1.1' }); } else { text = decodeSafe64Bson(text); // remove version byte before deserializing return BSON.deserialize(_buffer.from(text, 'base64').slice(1), { promoteBuffers: true }); } } /** * The Ruby version uses url safe base64 encoding. * RFC 4648 specifies + is encoded as - and / is _ * with the trailing = removed. */ export function encodeSafe64(data) { return encode64(data) .replace(/\+/g, '-') // Convert '+' to '-' .replace(/\//g, '_'); // Convert '/' to '_' // Not we don't remove the trailing '=' as specified in the spec // because ruby's Base64.urlsafe_encode64 does not do this // and we want to maintain compatibility. } export function decodeSafe64(base64) { return decode64(base64 .replace(/-/g, '+') // Convert '+' to '-' .replace(/_/g, '/')); // Don't bother concatenating an '=' to the result - see above } export function encodeSafe64Bson(versionByte, artifacts) { const bsonSerialized = _buffer.concat([ _buffer.from(versionByte), _buffer.from(BSON.serialize(artifacts)), ]); const base64Data = bsonSerialized.toString('base64'); return base64Data .replace(/\+/g, '-') // Convert '+' to '-' .replace(/\//g, '_'); // Convert '/' to '_' // Not we don't remove the trailing '=' as specified in the spec // because ruby's Base64.urlsafe_encode64 does not do this // and we want to maintain compatibility. } export function decodeSafe64Bson(base64) { return base64 .replace(/-/g, '+') // Convert '+' to '-' .replace(/_/g, '/'); // Don't bother concatenating an '=' to the result - see above } export function encodeDerivationArtifacts(artifacts) { return encodeSafe64(JSON.stringify(artifacts)); } export function decodeDerivationArtifacts(encoded) { return JSON.parse(decodeSafe64(encoded)); } /** * Returns some base64 encoded random bytes that can be used for encryption verification. */ export function generateEncryptionVerificationArtifacts() { const token = random.getBytesSync(16); const salt = random.getBytesSync(16); return { token: encodeSafe64(token), salt: encodeSafe64(salt), }; } export function keyLengthFromPublicKeyPem(publicKeyPem) { const pk = pki.publicKeyFromPem(publicKeyPem); // Undocumented functionality but was the only way I could find to get // key length out of the public key. // https://github.com/digitalbazaar/forge/blob/master/lib/rsa.js#L1244 const bitLength = pk.n.bitLength(); return bitLength; } export function keyLengthFromPrivateKeyPem(privateKey) { const pk = pki.privateKeyFromPem(privateKey); // Undocumented functionality but was the only way I could find to get // key length out of the public key. // https://github.com/digitalbazaar/forge/blob/master/lib/rsa.js#L1244 const bitLength = pk.n.bitLength(); return bitLength; } //# sourceMappingURL=util.js.map