# HG changeset patch # User Dan Fuhry # Date 1460388210 14400 # Node ID b9eb748ac1e485794b923ae23ddc1bdb42bbf1f4 # Parent 351d40b21cbc2b584703d5d88aa8001fb52f94e4 Add CSV based batch Yubikey registration diff -r 351d40b21cbc -r b9eb748ac1e4 YubikeyManagement.php --- a/YubikeyManagement.php Fri Apr 08 17:23:16 2016 -0400 +++ b/YubikeyManagement.php Mon Apr 11 11:23:30 2016 -0400 @@ -92,6 +92,7 @@ msg_no_yubikeys: 'No Yubikeys found', msg_editing_zero: 'Notice: You are currently viewing the YMS profile for Client ID 0, the pool of claimable keys. By default, anybody can validate or claim these Yubikeys, but you can prevent validation of these keys by marking them inactive here. All key settings such as lifecycle state and notes are reset when a user claims a key here.', btn_add_key: 'Add Yubikey', + btn_add_batch: 'Batch upload Yubikeys', btn_add_key_preregistered: 'Claim a New Key', btn_switch_to_zero: 'Edit claimable pool', btn_switch_from_zero: 'Switch back to my client', @@ -129,6 +130,41 @@ err_addkey_invalid_otp: 'The OTP from the Yubikey is invalid.', err_addkey_key_exists: 'This Yubikey is already registered on this server.', + // Batch add key interface + lbl_add_batch_heading: 'Batch upload new Yubikeys', + lbl_add_batch_desc: '

Using this form you can upload a CSV file containing any number of new Yubikeys to add.

+

Binary columns may be in either hexadecimal or ModHex format. If the format is ambiguous, hexadecimal will be assumed.

+

The header for the CSV must specify which columns are included. See below:

+ ', + lbl_add_batch_field_csv: 'Paste CSV:', + lbl_add_batch_field_csv_hint: 'See the format documentation above.', + btn_add_batch_submit: 'Register Yubikeys', + + err_add_batch_missing_aes_key: 'Column "aes_secret" is missing.', + err_add_batch_missing_id: 'You must provide either the "otp" column or "public_id" and "private_id".', + + lbl_add_batch_success_head: 'CSV processed successfully.', + err_add_batch_success: 'Line %line%, public ID %public_id%: success', + err_add_batch_bad_row_count: 'Line %line%: Missing or extra columns', + err_add_batch_aes_secret: 'Line %line%: AES key must be 16 bytes binary', + err_add_batch_bad_otp: 'Line %line%: Failed to decode OTP', + err_add_batch_bad_public_id: 'Line %line%: Public ID must be 6 bytes binary', + err_add_batch_bad_lifecycle_state: 'Line %line%: Lifecycle state must be "active" or "inactive"', + err_add_batch_bad_access: 'Line %line%: Access must be "global" or "restricted"', + err_add_batch_duplicate: 'Line %line%, public ID %public_id%: Duplicate Yubikey, this key is already registered in the system', + err_add_batch_query: 'Line %line%, public ID %public_id%: SQL error: %error%', + // Claim key interface lbl_claimkey_heading: 'Claim Yubikey', lbl_claimkey_desc: 'Attach a key you have not reprogrammed to your YMS account, so that you can see its AES secret key and keep track of it.', diff -r 351d40b21cbc -r b9eb748ac1e4 yms/yms.php --- a/yms/yms.php Fri Apr 08 17:23:16 2016 -0400 +++ b/yms/yms.php Mon Apr 11 11:23:30 2016 -0400 @@ -104,6 +104,201 @@ $result = yms_add_yubikey($_POST['add_aes'], $_POST['add_otp'], $client_id, $enabled, $any_client, $notes); yms_send_response('yms_msg_addkey_success', $result); } + else if ( isset($_POST['csv']) ) + { + $csv = explode("\n", trim($_POST['csv'])); + + $errors = array(); + + // first line: header + $head = str_getcsv($csv[0]); + + // column check: aes_secret + if ( !in_array('aes_secret', $head) ) + { + $errors[] = $lang->get('yms_err_add_batch_missing_aes_key'); + } + + // column check: otp, public_id and private_id + if ( !in_array('otp', $head) ) + { + if ( !in_array('public_id', $head) || !in_array('private_id', $head) ) + { + $errors[] = $lang->get('yms_err_add_batch_missing_id'); + } + } + + if ( !empty($errors) ) + { + yms_send_response(false, ''); + } + + // we are good to start processing + $db->transaction_begin(); + for ( $i = 1; $i < count($csv); $i++ ) + { + $line = str_getcsv($csv[$i]); + + // ensure column count == row count + if ( count($line) !== count($head) ) + { + $errors[] = $lang->get('yms_err_add_batch_bad_row_count', array('line' => $i)); + continue; + } + + // remap line + foreach ( $head as $j => $col ) + { + $line[$col] =& $line[$j]; + } + + // initialize row + $row = array( + 'client_id' => $yms_client_id, + 'aes_secret' => yms_hex_encode(yms_tobinary($line['aes_secret'])), + 'session_count' => 0, + 'token_count' => 0, + 'create_time' => time(), + 'token_time' => 0, + 'flags' => 0, + 'notes' => '' + ); + + if ( !preg_match('/^[0-9a-f]{32}$/', $row['aes_secret']) ) + { + $errors[] = $lang->get('yms_err_add_batch_aes_secret', array('line' => $i)); + continue; + } + + // do we have an OTP? + if ( isset($line['otp']) ) + { + // yes, decode it + $otp = yms_decode_otp($line['otp'], $line['aes_secret']); + if ( $otp === false ) + { + $errors[] = $lang->get('yms_err_add_batch_bad_otp', array('line' => $i)); + continue; + } + + if ( !$otp['crc_good'] ) + { + $errors[] = $lang->get('yms_err_add_batch_bad_otp', array('line' => $i)); + continue; + } + + $row['public_id'] = $otp['publicid']; + $row['private_id'] = $otp['privateid']; + $row['session_count'] = $otp['session']; + $row['token_count'] = $otp['count']; + $row['token_time'] = $otp['timestamp']; + } + + // public and private ID + foreach ( array('public_id', 'private_id') as $col ) + { + if ( !empty($line[$col]) ) + { + $row[$col] = yms_hex_encode(yms_tobinary($line[$col])); + } + if ( !isset($row[$col]) || !preg_match('/^[0-9a-f]{12}$/', $row[$col]) ) + { + $errors[] = $lang->get("yms_err_add_batch_bad_$col", array('line' => $i)); + continue 2; + } + } + + // session count, token count and timestamp + foreach ( array('session_count', 'token_count', 'token_time') as $col ) + { + if ( !empty($line[$col]) ) + { + $row[$col] = intval($line[$col]); + } + } + + // notes + if ( isset($line['notes']) ) + { + $row['notes'] = trim($line['notes']); + } + + // lifecycle state + if ( isset($line['lifecycle_state']) ) + { + if ( !in_array($line['lifecycle_state'], array('active', 'inactive')) ) + { + $errors[] = $lang->get('yms_err_add_batch_bad_lifecycle_state', array('line' => $i)); + continue; + } + + if ( $line['lifecycle_state'] === 'active' ) + { + $row['flags'] |= YMS_ENABLED; + } + } + else + { + // default to active + $row['flags'] |= YMS_ENABLED; + } + + // global access + if ( isset($line['access']) ) + { + if ( !in_array($line['access'], array('global', 'restricted')) ) + { + $errors[] = $lang->get('yms_err_add_batch_bad_access', array('line' => $i)); + continue; + } + + if ( $line['access'] === 'global' ) + { + $row['flags'] |= YMS_ANY_CLIENT; + } + } + + // duplicate key check + $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$row['public_id']}';"); + if ( $db->numrows() > 0 ) + { + $errors[] = $lang->get('yms_err_add_batch_duplicate', array('line' => $i, 'public_id' => $row['public_id'])); + continue; + } + + // build query + $cols = implode(', ', array_keys($row)); + + foreach ( $row as &$cell ) + { + if ( is_string($cell) ) + { + $cell = "'" . $db->escape($cell) . "'"; + } + } + unset($cell); + + $query = sprintf("INSERT INTO %syms_yubikeys ( %s ) VALUES ( %s );", + table_prefix, + $cols, + implode(', ', $row) + ); + + // insert it! + $q = $db->sql_query($query); + if ( $q ) + { + $errors[] = $lang->get('yms_err_add_batch_success', array('line' => $i, 'public_id' => $row['public_id'])); + } + else + { + $errors[] = $lang->get('yms_err_add_batch_query', array('line' => $i, 'public_id' => $row['public_id'], 'error' => $db->sql_error())); + } + } + $db->transaction_commit(); + + yms_send_response('

' . $lang->get('yms_lbl_add_batch_success_head') . '

', true); + } else if ( isset($_POST['claim_otp']) && getConfig('yms_claim_enable', 0) == 1 ) { // do we need to validate a custom field? @@ -177,6 +372,10 @@ href="" onclick="yms_showpage('AddKey'); return false;"> get('yms_btn_add_key'); ?> + + get('yms_btn_add_batch'); ?> + 0 ): ?> @@ -388,6 +587,53 @@ $output->footer(); } +// Add multiple Yubikeys by uploading a CSV +function page_Special_YMS_AddKeyBatch() +{ + global $db, $session, $paths, $template, $plugins; // Common objects + global $lang, $output; + + $output->add_after_header(''); + + $output->header(); + ?> + +

get('yms_lbl_add_batch_heading'); ?>

+ get('yms_lbl_add_batch_desc'); ?> + +
+ +
+ + + + + + + + + + + +
+ get('yms_lbl_add_batch_field_csv'); ?>
+ get('yms_lbl_add_batch_field_csv_hint'); ?> +
+ +
+ +
+
+ +
+ + footer(); +} + // Add key, using just an OTP // Requires the key to be in the database as client ID 0 function page_Special_YMS_AddPreregisteredKey() @@ -855,9 +1101,6 @@ if ( !$q ) $db->die_json(); - if ( $db->sql_affectedrows() < 1 ) - echo 'no affected rows; not '; - echo 'ok'; }