UNPKG

ubiq-security

Version:

Ubiq Client Node.js Implementation

445 lines (368 loc) 17.5 kB
const forge = require('node-forge'); const { FF1 } = require('./structured/FF1'); const { bigint_get_str, bigint_set_str } = require('./structured/Bn'); const { FfsCacheManager } = require('./ffsCacheManager'); const { FpeCacheManager } = require('./fpeCacheManager'); const { UbiqWebServices } = require('./ubiqWebServices'); const { BillingEventsProcessor } = require('./billingEventsProcessor'); const { Ff1CacheManager } = require('./ff1CacheManager'); const { BillingEvents } = require('./billingEvents'); const { Configuration } = require('./configuration'); const { Passthrough_Priorities } = require('./ffsCacheManager') const verbose = false; class StructuredEncryptDecrypt { constructor({ ubiqCredentials, ubiqConfiguration }) { this.ubiqCredentials = ubiqCredentials; if (!ubiqConfiguration) { this.ubiqConfiguration = new Configuration(); } else { this.ubiqConfiguration = ubiqConfiguration; } this.isInited = false; this.ubiqWebServices = new UbiqWebServices(this.ubiqCredentials); // dataset used to create URL and the results is the dataset JSON from web call this.ffsCacheManager = new FfsCacheManager(this.ubiqCredentials, this.ubiqWebServices, this.ubiqConfiguration); // dataset and key number used to create URL and result is the JSON respons from the web call this.fpeCacheManager = new FpeCacheManager(this.ubiqCredentials, this.ubiqWebServices, this.ubiqConfiguration); // this.fpeTransactions = new FpeTransactionManager(); this.billing_events = new BillingEvents(this.ubiqConfiguration); // datasetname & key number - returns FF1. Keynumber could be undefined or null for active key this.Ff1CacheManager = new Ff1CacheManager(this.ubiqConfiguration); this.billingEventsProcessor = new BillingEventsProcessor(new UbiqWebServices(this.ubiqCredentials), this.billing_events, this.ubiqConfiguration); this.srsa = this.ubiqCredentials.secret_crypto_access_key; } // eslint doesn't like a "return await" so just to be sure, perform in two stpes async close() { if (verbose) console.log(`StructuredEncryptDecrypt close `); await this.billingEventsProcessor.close() this.billing_events = null; } // eslint doesn't like a "return await" so just to be sure, perform in two stpes async GetFfsConfigurationAsync(ffsname) { var x = await this.ffsCacheManager.GetAsync(ffsname); return x } // eslint doesn't like a "return await" so just to be sure, perform in two steps async GetFpeEncryptionKeyAsync(ffsname, keyNumber = null) { var x = await this.fpeCacheManager.GetAsync(ffsname, keyNumber); if (this.ubiqCredentials.isIdp()) { // Only getting private key here, so cert is still OK x.encrypted_private_key = this.ubiqCredentials.getEncryptedPrivateKey() } return x } EncodeKeyNum(ffs, keyNumber, str, position) { if (position < 0) { throw new Error(`Bad String decoding position for: ${str}`); } const strChars = str.split(''); const charBuf = strChars[position]; let ct_value = ffs.OutputCharacters.indexOf(charBuf); const msb_encoding_bits = ffs.MsbEncodingBits; ct_value += (keyNumber << msb_encoding_bits.Value); const ch = ffs.OutputCharacters.subString(ct_value, 1); strChars[position] = ch[0]; return String(strChars); } async GetFF1(ffs, keyNumber) { // Only check cache once { const ctx_and_key = this.Ff1CacheManager.Get(ffs.name, keyNumber); if (ctx_and_key) { if (verbose) { console.log("GetFF1: ctx_and_key", ctx_and_key) } return ctx_and_key; } } if (verbose) { console.log("GetFF1: ctx_and_key is not set") } // assume keynumber if -1 and check the cache with keynumber1+ffs and get the cache. : // example cache value = ff1 // get ffe + keynumber = -1 const fpe = await this.GetFpeEncryptionKeyAsync(ffs.name, keyNumber); // Get wrapped data key from response body // key_number is json field, but set value to activeKey variable const { encrypted_private_key, wrapped_data_key, key_number: activeKey } = fpe; var privateKey = forge.pki.decryptRsaPrivateKey(encrypted_private_key, this.srsa); if (verbose) console.log("GetFF1 private key", await forge.pki.privateKeyToPem(privateKey)) return await this.AddFF1(ffs, privateKey, wrapped_data_key, keyNumber, activeKey); } // Add to the cache. Key number may not always be the same as active key. async AddFF1(ffs, privateKey, wrapped_data_key, keyNumber, activeKey) { const wdk = forge.util.decode64(wrapped_data_key); const tweakUint8 = Uint8Array.from(Buffer.from(ffs.tweak, 'base64')); // Decrypt the encryped private key using @srsa supplied try { const decrypted = privateKey.decrypt(wdk, 'RSA-OAEP'); this.keyRaw = new Uint8Array(Buffer.from(decrypted, 'binary')); } catch (err) { throw new Error('Problem decrypting ENCRYPTED private key' + err); } const ctx = new FF1( this.keyRaw, tweakUint8, this.tweak_min_len, this.tweak_max_len, ffs.input_character_set.length, ffs.input_character_set, ); // If key number is NULL, then add this one again so we have for NULL and the actual active key await this.Ff1CacheManager.Set(ffs.name, keyNumber, { ctx, activeKey }); if (keyNumber === undefined || keyNumber == null) { await this.Ff1CacheManager.Set(ffs.name, activeKey, { ctx, activeKey }); } if (verbose) { console.log("AddFF1: { ctx, activeKey } ", { ctx, activeKey }) } return { ctx, activeKey }; } async EncryptAsync(ffsName, plainText, tweak) { try { let verbose = false; const ffs = await this.GetFfsConfigurationAsync(ffsName); // active key will be used during decryption const { ctx, activeKey } = await this.GetFF1(ffs, null); if (verbose) { console.log("EncryptAsync Ctx: ", ctx, activeKey) } var x = await this.EncryptAsyncKeyNumber(ctx, ffs, plainText, tweak, activeKey); return x } catch (ex) { throw new Error(ex.message); } }; parsePrefix(plainTextArr, prefix_length) { return [plainTextArr.slice(0, prefix_length), plainTextArr.slice(prefix_length)] } parsePrefixTrimmed(trimmed, formatted, prefix_length, passthrough) { let prefix_str = []; // Passthrough characters are ignored from the count, but added to prefix string for convenience let i = 0; while (i < prefix_length) { let ch = formatted.shift(); if (passthrough.has(ch) === true) { prefix_str.push(ch); } else { prefix_str.push(trimmed.shift()); i++; } } return [prefix_str, trimmed, formatted] } parseSuffixTrimmed(trimmed, formatted, suffix_length, passthrough) { let suffix_str = []; // Passthrough characters are ignored from the count, but added to prefix string for convenience let i = 0; while (i < suffix_length) { let ch = formatted.pop(); if (passthrough.has(ch) === true) { suffix_str.unshift(ch); } else { suffix_str.unshift(trimmed.pop()); i++; } } return [suffix_str, trimmed, formatted] } parseSuffix(plainTextArr, suffix_length) { return [plainTextArr.slice(plainTextArr.length - suffix_length), plainTextArr.slice(0, plainTextArr.length - suffix_length)] } trimInput(input, passthrough, zeroth_characer) { let trimmed = [] let formatted = [] for (const currentChar of input) { if (passthrough.has(currentChar) === false) { trimmed.push(currentChar); formatted.push(zeroth_characer); } else { formatted.push(currentChar); } } return [trimmed, formatted]; } async EncryptAsyncKeyNumber(ctx, ffs, plainText, tweak, keyNumber) { let verbose = false; let plainTextArr = plainText.split(''); const setInputChar = new Set(ffs.input_character_set.split('')); const setPassthrough = new Set(ffs.passthrough.split('')); let trimText = []; let formattedDestination = []; let prefix_str = []; let suffix_str = []; let passthrough_processed = false; for (const action of ffs.passthrough_priorities.values()) { if (action == Passthrough_Priorities.Prefix) { if (!passthrough_processed) { [prefix_str, plainTextArr] = this.parsePrefix(plainTextArr, ffs.prefix_length); if (verbose) console.log(`prefix_str: ${prefix_str}`); if (verbose) console.log(`plainTextArr: ${plainTextArr}`); } else { [prefix_str, trimText, formattedDestination] = this.parsePrefixTrimmed(trimText, formattedDestination, ffs.prefix_length, setPassthrough); if (verbose) console.log(`prefix_str: ${prefix_str}`); if (verbose) console.log(`trimText: ${trimText}`); if (verbose) console.log(`formattedDestination: ${formattedDestination}`); } } else if (action == Passthrough_Priorities.Suffix) { if (!passthrough_processed) { [suffix_str, plainTextArr] = this.parseSuffix(plainTextArr, ffs.suffix_length); if (verbose) console.log(`suffix_str: ${suffix_str}`); if (verbose) console.log(`plainTextArr: ${plainTextArr}`); } else { [suffix_str, trimText, formattedDestination] = this.parseSuffixTrimmed(trimText, formattedDestination, ffs.suffix_length, setPassthrough); if (verbose) console.log(`suffix_str: ${suffix_str}`); if (verbose) console.log(`trimText: ${trimText}`); if (verbose) console.log(`formattedDestination: ${formattedDestination}`); } } else if (action == Passthrough_Priorities.Passthrough) { passthrough_processed = true; [trimText, formattedDestination] = this.trimInput(plainTextArr, setPassthrough, ffs.output_character_set[0]) } } // Wasn't a partial encryption dataset, we still need to parse da if (!passthrough_processed) { [trimText, formattedDestination] = this.trimInput(plainTextArr, setPassthrough, ffs.output_character_set[0]) } // Validate trimmed is all from input characterset for (const ch of trimText) { if (setInputChar.has(ch) === false) { throw new Error(`invalid character found in the input:${ch}`); } } if (trimText.length < ffs.min_input_length || trimText.length > ffs.max_input_length) { throw new Error(`Invalid input len min: ${ffs.min_input_length} max: ${ffs.max_input_length}`); } if (verbose) { console.log("Ctx: ", ctx) } const encrypted = ctx.encrypt(trimText.join('')); const bigNum1 = bigint_set_str(encrypted, ffs.input_character_set); const cipherText = bigint_get_str(ffs.output_character_set, bigNum1); const cipherTextPad = cipherText.padStart(trimText.length, ffs.output_character_set[0]); const keyNumIndex = ffs.output_character_set.indexOf(cipherTextPad[0]); const ct_value = keyNumIndex + (parseInt(keyNumber, 10) << ffs.msb_encoding_bits); const cipherTextPadArr = cipherTextPad.split(''); cipherTextPadArr[0] = ffs.output_character_set[ct_value]; let k = 0; for (let i = 0; i < formattedDestination.length; i++) { if (formattedDestination[i] === ffs.output_character_set[0]) { formattedDestination[i] = cipherTextPadArr[k]; k++; } } const be = await this.billing_events.addBillingEvent(this.ubiqCredentials.access_key_id, ffs.name, "", BillingEvents.ENCRYPTION, BillingEvents.STRUCTURED, keyNumber, 1); return prefix_str.concat(formattedDestination).concat(suffix_str).join(''); } async DecryptAsync(ffsName, cipherText, tweak) { try { let verbose = false; const ffs = await this.GetFfsConfigurationAsync(ffsName); let cipherTextPadArr = cipherText.split(''); const setOutputChar = new Set(ffs.output_character_set.split('')); const setPassthrough = new Set(ffs.passthrough.split('')); let trimText = []; let formattedDestination = []; let prefix_str = []; let suffix_str = []; let passthrough_processed = false; for (const action of ffs.passthrough_priorities.values()) { if (action == Passthrough_Priorities.Prefix) { if (!passthrough_processed) { [prefix_str, cipherTextPadArr] = this.parsePrefix(cipherTextPadArr, ffs.prefix_length); if (verbose) console.log(`prefix_str: ${prefix_str}`); if (verbose) console.log(`cipherTextPadArr: ${cipherTextPadArr}`); } else { [prefix_str, trimText, formattedDestination] = this.parsePrefixTrimmed(trimText, formattedDestination, ffs.prefix_length, setPassthrough); if (verbose) console.log(`prefix_str: ${prefix_str}`); if (verbose) console.log(`trimText: ${trimText}`); if (verbose) console.log(`formattedDestination: ${formattedDestination}`); } } else if (action == Passthrough_Priorities.Suffix) { if (!passthrough_processed) { [suffix_str, cipherTextPadArr] = this.parseSuffix(cipherTextPadArr, ffs.suffix_length); if (verbose) console.log(`suffix_str: ${suffix_str}`); if (verbose) console.log(`cipherTextPadArr: ${cipherTextPadArr}`); } else { [suffix_str, trimText, formattedDestination] = this.parseSuffixTrimmed(trimText, formattedDestination, ffs.suffix_length, setPassthrough); if (verbose) console.log(`suffix_str: ${suffix_str}`); if (verbose) console.log(`trimText: ${trimText}`); if (verbose) console.log(`formattedDestination: ${formattedDestination}`); } } else if (action == Passthrough_Priorities.Passthrough) { passthrough_processed = true; [trimText, formattedDestination] = this.trimInput(cipherTextPadArr, setPassthrough, ffs.input_character_set[0]) } } // Wasn't a partial encryption dataset, we still need to parse data if (!passthrough_processed) { [trimText, formattedDestination] = this.trimInput(cipherTextPadArr, setPassthrough, ffs.input_character_set[0]) } // Validate trimmed is all from input characterset for (const ch of trimText) { if (setOutputChar.has(ch) === false) { throw new Error(`invalid character found in the input:${ch}`); } } let first = ffs.output_character_set.indexOf(trimText[0]); const activeKey = first >> ffs.msb_encoding_bits; first -= (activeKey << ffs.msb_encoding_bits); trimText[0] = ffs.output_character_set[first]; // active key will be used during decryption const { ctx } = await this.GetFF1(ffs, activeKey); if (verbose) { console.log("DecryptAsync Ctx: ", ctx) } const bigNum1 = bigint_set_str( trimText.join(''), ffs.output_character_set, ); const plainText = bigint_get_str(ffs.input_character_set, bigNum1); const plainTextPad = plainText.padStart( trimText.length, ffs.input_character_set[0], ); const plainTextValue = ctx.decrypt(plainTextPad); let k = 0; for (let i = 0; i < formattedDestination.length; i++) { if (formattedDestination[i] === ffs.input_character_set[0]) { formattedDestination[i] = plainTextValue[k]; k++; } } // const decryptedPlainText = formattedDestination.join(''); const be = this.billing_events.addBillingEvent(this.ubiqCredentials.access_key_id, ffsName, "", BillingEvents.DECRYPTION, BillingEvents.STRUCTURED, activeKey, 1); // this.billingEventsProcessor.close(); return prefix_str.concat(formattedDestination).concat(suffix_str).join(''); } catch (ex) { throw new Error(ex.message); } } async EncryptForSearchAsync(ffsName, plainText, tweak) { try { // Will return the array of keys from 0 .. current_key unless the data key has been rotated too many times const data = await this.ubiqWebServices.GetFFSAndDataKeys(ffsName); var ffs = data[ffsName]['ffs'] this.ffsCacheManager.AddToCache(ffsName, ffs) var encrypted_private_key = data[ffsName]['encrypted_private_key'] var privateKey = forge.pki.decryptRsaPrivateKey(encrypted_private_key, this.srsa); var current_key_number = data[ffsName]['current_key_number'] var keys = data[ffsName]['keys'] // Add for active key (null) and actual current_key_number. await this.AddFF1(ffs, privateKey, keys[current_key_number], null, current_key_number) let ct = [] // Add of the the Dataset keys to the cache and calculate the cipher text for (let i = 0; i < keys.length; i++) { var ctx = null const ctx_and_key = this.Ff1CacheManager.Get(ffs.name, i) if (!ctx_and_key) { let ctx2 = await this.AddFF1(ffs, privateKey, keys[i], i, i) ctx = ctx2.ctx } else { ctx = ctx_and_key.ctx } ct.push(await this.EncryptAsyncKeyNumber(ctx, ffs, plainText, tweak, i)) } return ct } catch (ex) { throw new Error(ex.message); } } addReportingUserDefinedMetadata(jsonString) { this.billing_events.addUserDefinedMetadata(jsonString); } getCopyOfUsage() { return this.billing_events.getSerializedData(); } } module.exports = { StructuredEncryptDecrypt };