Skip to content

Commit

Permalink
Replace FNV32a pad's id generator with salted SHA1
Browse files Browse the repository at this point in the history
When managing Etherpad's pads, Meteor makes API calls to initiate the closed captions
and shared notes modules. The pad id was being mapped to a shorter id than the meeting
id because of a Etherpad lenght limitation.

Changed to something less guessable.
  • Loading branch information
pedrobmarin committed Feb 9, 2021
1 parent 705ea99 commit c0a7f9c
Show file tree
Hide file tree
Showing 21 changed files with 60 additions and 79 deletions.
15 changes: 7 additions & 8 deletions bigbluebutton-html5/imports/api/captions/server/helpers.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { Meteor } from 'meteor/meteor';
import { hashFNV32a } from '/imports/api/common/server/helpers';
import { hashSHA1 } from '/imports/api/common/server/helpers';
import { check } from 'meteor/check';

const ETHERPAD = Meteor.settings.private.etherpad;
const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const BASENAME = Meteor.settings.public.app.basename;
const APP = Meteor.settings.private.app;
const LOCALES_URL = `http://${APP.host}:${APP.port}${BASENAME}${APP.localesUrl}`;
const CAPTIONS = '_captions_';
const CAPTIONS_TOKEN = '_cc_';
const TOKEN = '$';

// Captions padId should look like: {padId}_captions_{locale}
const generatePadId = (meetingId, locale) => {
const padId = `${hashFNV32a(meetingId, true)}${CAPTIONS}${locale}`;
return padId;
};
// Captions padId should look like: {prefix}_cc_{locale}
const generatePadId = (meetingId, locale) => `${hashSHA1(meetingId+locale+ETHERPAD.apikey)}${CAPTIONS_TOKEN}${locale}`;

const isCaptionsPad = (padId) => {
const splitPadId = padId.split(CAPTIONS);
const splitPadId = padId.split(CAPTIONS_TOKEN);
return splitPadId.length === 2;
};

Expand Down Expand Up @@ -45,6 +43,7 @@ const processForCaptionsPadOnly = fn => (message, ...args) => {
};

export {
CAPTIONS_TOKEN,
generatePadId,
processForCaptionsPadOnly,
isEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export default function appendText(text, locale) {
responseType: 'json',
}).then((response) => {
const { status } = response;
if (status === 200) {
Logger.verbose('Captions: appended text', { padId });
if (status !== 200) {
Logger.error(`Could not append captions for padId=${padId}`);
return;
}
}).catch(error => Logger.error(`Could not append captions for padId=${padId}: ${error}`));
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export default function createCaptions(meetingId) {
const { status } = response;
if (status !== 200) {
Logger.error(`Could not get locales info for ${meetingId} ${status}`);
return;
}
const locales = response.data;
locales.forEach((locale) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ export default function editCaptions(padId, data) {
return;
}


const {
meetingId,
ownerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ export default function fetchReadOnlyPadId(padId) {
check(padId, String);

const readOnlyURL = getReadOnlyIdURL(padId);

axios({
method: 'get',
url: readOnlyURL,
responseType: 'json',
}).then((response) => {
const { status } = response;
if (status !== 200) {
Logger.error(`Could not get closed captions readOnlyID for ${padId} ${status}`);
return;
}
const readOnlyPadId = getDataFromResponse(response.data, 'readOnlyID');
if (readOnlyPadId) {
updateReadOnlyPadId(padId, readOnlyPadId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { check } from 'meteor/check';
import Captions from '/imports/api/captions';
import updateOwnerId from '/imports/api/captions/server/modifiers/updateOwnerId';
import { extractCredentials } from '/imports/api/common/server/helpers';
import { CAPTIONS_TOKEN } from '/imports/api/captions/server/helpers';

export default function takeOwnership(locale) {
const { meetingId, requesterUserId } = extractCredentials(this.userId);

check(locale, String);

const pad = Captions.findOne({ meetingId, padId: { $regex: `_captions_${locale}$` } });
const pad = Captions.findOne({ meetingId, padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` } });

if (pad) {
updateOwnerId(meetingId, requesterUserId, pad.padId);
Expand Down
27 changes: 2 additions & 25 deletions bigbluebutton-html5/imports/api/common/server/helpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import sha1 from 'crypto-js/sha1';
import Users from '/imports/api/users';

const MSG_DIRECT_TYPE = 'DIRECT';
Expand Down Expand Up @@ -38,31 +39,7 @@ export const processForHTML5ServerOnly = fn => (message, ...args) => {
return fn(message, ...args);
};

/**
* Calculate a 32 bit FNV-1a hash
* Found here: https://gist.github.com/vaiorabbit/5657561
* Ref.: http://isthe.com/chongo/tech/comp/fnv/
*
* @param {string} str the input value
* @param {boolean} [asString=false] set to true to return the hash value as
* 8-digit hex string instead of an integer
* @param {integer} [seed] optionally pass the hash of the previous chunk
* @returns {integer | string}
*/
/* eslint-disable */
export const hashFNV32a = (str, asString, seed) => {
let hval = (seed === undefined) ? 0x811c9dc5 : seed;

for (let i = 0, l = str.length; i < l; i++) {
hval ^= str.charCodeAt(i);
hval += (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24);
}
if (asString) {
return (`0000000${(hval >>> 0).toString(16)}`).substr(-8);
}
return hval >>> 0;
};
/* eslint-enable */
export const hashSHA1 = (str) => sha1(str).toString();

export const extractCredentials = (credentials) => {
if (!credentials) return {};
Expand Down
7 changes: 2 additions & 5 deletions bigbluebutton-html5/imports/api/note/server/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { hashFNV32a } from '/imports/api/common/server/helpers';
import { hashSHA1 } from '/imports/api/common/server/helpers';

const ETHERPAD = Meteor.settings.private.etherpad;
const NOTE_CONFIG = Meteor.settings.public.note;
Expand All @@ -12,10 +12,7 @@ const getReadOnlyIdURL = padId => `${BASE_URL}/getReadOnlyID?apikey=${ETHERPAD.a

const appendTextURL = (padId, text) => `${BASE_URL}/appendText?apikey=${ETHERPAD.apikey}&padID=${padId}&text=${encodeURIComponent(text)}`;

const generateNoteId = (meetingId) => {
const noteId = hashFNV32a(meetingId, true);
return noteId;
};
const generateNoteId = (meetingId) => hashSHA1(meetingId+ETHERPAD.apikey);

const isEnabled = () => NOTE_CONFIG.enabled;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export default function createNote(meetingId) {
const noteId = generateNoteId(meetingId);

const createURL = createPadURL(noteId);

axios({
method: 'get',
url: createURL,
Expand All @@ -30,6 +31,7 @@ export default function createNote(meetingId) {
const { status } = responseOuter;
if (status !== 200) {
Logger.error(`Could not get note info for ${meetingId} ${status}`);
return;
}
const readOnlyURL = getReadOnlyIdURL(noteId);
axios({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@ const getLang = () => {
};

const getPadParams = () => {
const { config } = NOTE_CONFIG;
const User = Users.findOne({ userId: Auth.userID }, { fields: { name: 1, color: 1 } });
config.userName = User.name;
config.userColor = User.color;
let config = {};
config.lang = getLang();
config.rtl = document.documentElement.getAttribute('dir') === 'rtl';

const params = [];
Object.keys(config).forEach((k) => {
Expand All @@ -26,12 +24,12 @@ const getPadParams = () => {

const getPadURL = (padId, readOnlyPadId, ownerId) => {
const userId = Auth.userID;
const params = getPadParams();
let url;
if (!ownerId || (ownerId && userId === ownerId)) {
const params = getPadParams();
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${padId}?${params}`);
} else {
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}`);
url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyPadId}?${params}`);
}
return url;
};
Expand Down
5 changes: 3 additions & 2 deletions bigbluebutton-html5/imports/ui/components/captions/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Meteor } from 'meteor/meteor';
import { Session } from 'meteor/session';

const CAPTIONS_CONFIG = Meteor.settings.public.captions;
const CAPTIONS = '_captions_';
const CAPTIONS_TOKEN = '_cc_';
const LINE_BREAK = '\n';
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;

Expand All @@ -19,7 +19,7 @@ const getActiveCaptions = () => {

const getCaptions = locale => Captions.findOne({
meetingId: Auth.meetingID,
padId: { $regex: `${CAPTIONS}${locale}$` },
padId: { $regex: `${CAPTIONS_TOKEN}${locale}$` },
});

const getCaptionsData = () => {
Expand Down Expand Up @@ -170,6 +170,7 @@ const initSpeechRecognition = (locale) => {
};

export default {
CAPTIONS_TOKEN,
getCaptionsData,
getAvailableLocales,
getOwnedLocales,
Expand Down
18 changes: 8 additions & 10 deletions bigbluebutton-html5/imports/ui/components/note/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@ const getLang = () => {
};

const getNoteParams = () => {
const { config } = NOTE_CONFIG;
const User = Users.findOne({ userId: Auth.userID }, { fields: { name: 1, color: 1 } });
config.userName = User.name;
config.userColor = User.color;
let config = {};
config.lang = getLang();
config.rtl = document.documentElement.getAttribute('dir') === 'rtl';

const params = [];
for (const key in config) {
if (config.hasOwnProperty(key)) {
params.push(`${key}=${encodeURIComponent(config[key])}`);
}
}
Object.keys(config).forEach((k) => {
params.push(`${k}=${encodeURIComponent(config[k])}`);
});

return params.join('&');
};

Expand All @@ -51,7 +48,8 @@ const isLocked = () => {

const getReadOnlyURL = () => {
const readOnlyNoteId = getReadOnlyNoteId();
const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyNoteId}`);
const params = getNoteParams();
const url = Auth.authenticateURL(`${NOTE_CONFIG.url}/p/${readOnlyNoteId}?${params}`);
return url;
};

Expand Down
5 changes: 5 additions & 0 deletions bigbluebutton-html5/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions bigbluebutton-html5/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"browser-detect": "^0.2.28",
"classnames": "^2.2.6",
"clipboard": "^2.0.4",
"crypto-js": "^4.0.0",
"eventemitter2": "~5.0.1",
"fastdom": "^1.0.9",
"fibers": "^3.1.1",
Expand Down
6 changes: 0 additions & 6 deletions bigbluebutton-html5/private/config/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,6 @@ public:
note:
enabled: false
url: ETHERPAD_HOST
config:
showLineNumbers: false
showChat: false
noColors: true
showControls: true
rtl: false
layout:
autoSwapLayout: false
hidePresentation: false
Expand Down
1 change: 0 additions & 1 deletion record-and-playback/core/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ source "https://rubygems.org"
gem "absolute_time"
gem "builder"
gem "fastimage"
gem "fnv"
gem "java_properties"
gem "journald-logger"
gem "jwt"
Expand Down
2 changes: 0 additions & 2 deletions record-and-playback/core/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ GEM
crass (1.0.5)
fastimage (2.1.5)
ffi (1.11.1)
fnv (0.2.0)
jaro_winkler (1.5.2)
java_properties (0.0.4)
journald-logger (2.0.4)
Expand Down Expand Up @@ -48,7 +47,6 @@ DEPENDENCIES
absolute_time
builder
fastimage
fnv
java_properties
journald-logger
jwt
Expand Down
9 changes: 5 additions & 4 deletions record-and-playback/core/lib/recordandplayback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
require 'find'
require 'rubygems'
require 'net/http'
require 'fnv'
require 'digest'
require 'shellwords'
require 'English'

Expand Down Expand Up @@ -226,9 +226,10 @@ def self.record_id_to_timestamp(r)
r.split("-")[1].to_i / 1000
end

# Notes id will be an 8-sized hash string based on the meeting id
def self.get_notes_id(meeting_id)
FNV.new.fnv1a_32(meeting_id).to_s(16).rjust(8, '0')
# Notes id will be a SHA1 hash string based on the meeting id and etherpad's apikey
def self.get_notes_id(meeting_id, notes_apikey)
value = meeting_id + notes_apikey
Digest::SHA1.hexdigest value
end

def self.done_to_timestamp(r)
Expand Down
7 changes: 4 additions & 3 deletions record-and-playback/core/scripts/archive/archive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ def archive_events(meeting_id, redis_host, redis_port, redis_password, raw_archi
end
end

def archive_notes(meeting_id, notes_endpoint, notes_formats, raw_archive_dir)
def archive_notes(meeting_id, notes_endpoint, notes_formats, notes_apikey, raw_archive_dir)
BigBlueButton.logger.info("Archiving notes for #{meeting_id}")
notes_dir = "#{raw_archive_dir}/#{meeting_id}/notes"
FileUtils.mkdir_p(notes_dir)
notes_id = BigBlueButton.get_notes_id(meeting_id)
notes_id = BigBlueButton.get_notes_id(meeting_id, notes_apikey)

tmp_note = "#{notes_dir}/tmp_note.txt"
BigBlueButton.try_download("#{notes_endpoint}/#{notes_id}/export/txt", tmp_note)
Expand Down Expand Up @@ -180,6 +180,7 @@ def archive_has_recording_marks?(meeting_id, raw_archive_dir, break_timestamp)
log_dir = props['log_dir']
notes_endpoint = props['notes_endpoint']
notes_formats = props['notes_formats']
notes_apikey = props['notes_apikey']

# Determine the filenames for the done and fail files
if !break_timestamp.nil?
Expand All @@ -198,7 +199,7 @@ def archive_has_recording_marks?(meeting_id, raw_archive_dir, break_timestamp)
# FreeSWITCH Audio files
archive_audio(meeting_id, audio_dir, raw_archive_dir)
# Etherpad notes
archive_notes(meeting_id, notes_endpoint, notes_formats, raw_archive_dir)
archive_notes(meeting_id, notes_endpoint, notes_formats, notes_apikey, raw_archive_dir)
# Presentation files
archive_directory("#{presentation_dir}/#{meeting_id}/#{meeting_id}", "#{target_dir}/presentation")
# Red5 media
Expand Down
1 change: 1 addition & 0 deletions record-and-playback/core/scripts/bigbluebutton.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ raw_webrtc_deskshare_src: /usr/share/red5/webapps/video-broadcast/streams
raw_deskshare_src: /var/bigbluebutton/deskshare
raw_presentation_src: /var/bigbluebutton
notes_endpoint: http://localhost:9001/p
notes_apikey: ETHERPAD_APIKEY
# Specify the notes formats we archive
# txt, doc and odt are also supported
notes_formats:
Expand Down
Loading

0 comments on commit c0a7f9c

Please sign in to comment.