@meeco/cryppo
Version:
In-browser encryption and decryption. Clone of Ruby Cryppo
200 lines • 7.71 kB
JavaScript
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