commit 154562e9612542544c709ab4f2260f4771f5ba22 Author: Knut Ahlers Date: Wed Jun 14 20:40:12 2017 +0200 Initial version Signed-off-by: Knut Ahlers diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c8d092 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.env +vault-otp-ui diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f758374 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ + +generate: build_js + go generate + +build_js: + coffee -c application.coffee diff --git a/application.coffee b/application.coffee new file mode 100644 index 0000000..8b32158 --- /dev/null +++ b/application.coffee @@ -0,0 +1,69 @@ +currentTimeout = 0 +clipboard = undefined + +$ -> + if $('body').hasClass 'state-signedin' + initializeApplication() + +createOTPItem = (item) -> + tpl = $('#otp-item').html() + + otpItem = $(tpl) + otpItem.find('.badge').text item.code.replace(/^(.{3})(.{3})$/, '$1 $2') + otpItem.find('.title').text item.name + + otpItem.appendTo $('#keylist') + +delay = (delayMSecs, fkt) -> + window.setTimeout fkt, delayMSecs + +fetchCodes = () -> + $.ajax + url: 'codes.json', + success: updateCodes, + dataType: 'json', + +filterChange = () -> + filter = $('#filter').val().toLowerCase() + $('.otp-item').each (idx, el) -> + if $(el).find('.title').text().toLowerCase().match(filter) == null + $(el).hide() + else + $(el).show() + +initializeApplication = () -> + $('#keylist').empty() + $('#filter').bind 'keyup', filterChange + tick 500, refreshTimerProgress + fetchCodes() + +# refreshTimerProgress updates the top progressbar to display the +# remaining time until the one-time-passwords changes +refreshTimerProgress = () -> + secondsLeft = timeLeft() + $('#timer').css 'width', "#{secondsLeft / 30 * 100}%" + +tick = (delay, fkt) -> + window.setInterval fkt, delay + +timeLeft = () -> + now = new Date().getTime() + (currentTimeout - now) / 1000 + +updateCodes = (data) -> + currentTimeout = new Date(data.next_wrap).getTime() + + if clipboard + clipboard.destroy() + + $('#keylist').empty() + for token in data.tokens + createOTPItem token + + clipboard = new Clipboard '.otp-item', + text: (trigger) -> + $(trigger).find('.badge').text().replace(' ', '') + + filterChange() + + delay timeLeft()*1000, fetchCodes diff --git a/application.js b/application.js new file mode 100644 index 0000000..c5935c2 --- /dev/null +++ b/application.js @@ -0,0 +1,92 @@ +// Generated by CoffeeScript 1.12.4 +(function() { + var clipboard, createOTPItem, currentTimeout, delay, fetchCodes, filterChange, initializeApplication, refreshTimerProgress, tick, timeLeft, updateCodes; + + currentTimeout = 0; + + clipboard = void 0; + + $(function() { + if ($('body').hasClass('state-signedin')) { + return initializeApplication(); + } + }); + + createOTPItem = function(item) { + var otpItem, tpl; + tpl = $('#otp-item').html(); + otpItem = $(tpl); + otpItem.find('.badge').text(item.code.replace(/^(.{3})(.{3})$/, '$1 $2')); + otpItem.find('.title').text(item.name); + return otpItem.appendTo($('#keylist')); + }; + + delay = function(delayMSecs, fkt) { + return window.setTimeout(fkt, delayMSecs); + }; + + fetchCodes = function() { + return $.ajax({ + url: 'codes.json', + success: updateCodes, + dataType: 'json' + }); + }; + + filterChange = function() { + var filter; + filter = $('#filter').val().toLowerCase(); + return $('.otp-item').each(function(idx, el) { + if ($(el).find('.title').text().toLowerCase().match(filter) === null) { + return $(el).hide(); + } else { + return $(el).show(); + } + }); + }; + + initializeApplication = function() { + $('#keylist').empty(); + $('#filter').bind('keyup', filterChange); + tick(500, refreshTimerProgress); + return fetchCodes(); + }; + + refreshTimerProgress = function() { + var secondsLeft; + secondsLeft = timeLeft(); + return $('#timer').css('width', (secondsLeft / 30 * 100) + "%"); + }; + + tick = function(delay, fkt) { + return window.setInterval(fkt, delay); + }; + + timeLeft = function() { + var now; + now = new Date().getTime(); + return (currentTimeout - now) / 1000; + }; + + updateCodes = function(data) { + var i, len, ref, token; + currentTimeout = new Date(data.next_wrap).getTime(); + if (clipboard) { + clipboard.destroy(); + } + $('#keylist').empty(); + ref = data.tokens; + for (i = 0, len = ref.length; i < len; i++) { + token = ref[i]; + createOTPItem(token); + } + clipboard = new Clipboard('.otp-item', { + text: function(trigger) { + return $(trigger).find('.badge').text().replace(' ', ''); + } + }); + filterChange(); + return delay(timeLeft() * 1000, fetchCodes); + }; + +}).call(this); diff --git a/assets.go b/assets.go new file mode 100644 index 0000000..a1db797 --- /dev/null +++ b/assets.go @@ -0,0 +1,258 @@ +// Code generated by go-bindata. +// sources: +// index.html +// application.js +// DO NOT EDIT! + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _indexHtml = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xd4\x58\x69\x73\xdb\x38\xef\x7f\xdd\x7c\x0a\x54\x9d\x9d\xdd\xed\xae\x2c\x1f\x71\x0e\x47\xf6\x4c\x9a\xa4\xb9\x9b\xe6\x6c\x93\x9d\x7d\x41\x49\x90\xc4\x84\x22\x55\x92\xf2\x91\x4c\xbf\xfb\x7f\xa8\xc3\x96\x1d\xb7\xcd\x76\xff\xcf\x33\xf3\xb4\x33\xb1\x08\x01\x20\xf0\x03\x08\x80\x72\x5f\xef\x9e\xed\x5c\xdd\x7e\xdc\x83\x58\x27\x6c\xb0\xe2\x9a\x1f\x60\x84\x47\x7d\x0b\xb9\x35\x58\x01\x70\x63\x24\x81\x79\x00\x70\x13\xd4\x04\xfc\x98\x48\x85\xba\x6f\x65\x3a\xb4\x37\xac\xfa\xab\x58\xeb\xd4\xc6\x2f\x19\x1d\xf6\xad\xcf\xf6\xf5\xb6\xbd\x23\x92\x94\x68\xea\x31\xb4\xc0\x17\x5c\x23\xd7\x7d\xeb\x70\xaf\x8f\x41\x84\x73\x92\x9c\x24\xd8\xb7\x86\x14\x47\xa9\x90\xba\xc6\x3c\xa2\x81\x8e\xfb\x01\x0e\xa9\x8f\x76\xbe\xf8\x13\x28\xa7\x9a\x12\x66\x2b\x9f\x30\xec\xb7\x2a\x45\xaf\x6d\x1b\xae\x62\x04\xe2\x89\x21\x42\x07\x72\xc5\x9a\x44\x0a\xde\x26\x99\xd2\x6f\xc1\x17\x09\x42\x48\xa5\xd2\x40\x39\xe8\x18\xc1\xf8\xb6\x05\x84\x4f\x40\xe8\x18\x65\xbe\xae\xf6\x06\x23\x54\xc8\xbc\x25\xa1\x46\xf9\xd6\x88\x28\x2c\x54\xda\x76\xb9\xab\xa6\x9a\xe1\xe0\x86\x64\x4c\xc3\xd9\xd5\x47\xfb\xfa\xd0\x75\x0a\xda\xca\xcc\xac\x77\x42\x68\xa5\x25\x49\x67\x72\x8c\xf2\x07\x90\xc8\xfa\x96\xd2\x13\x86\x2a\x46\xd4\x16\xc4\x12\xc3\xbe\x65\x70\x54\x3d\xc7\xf1\x03\x7e\xaf\x1a\x3e\x13\x59\x10\x32\x22\xb1\xe1\x8b\xc4\x21\xf7\x64\xec\x30\xea\x29\x47\x8f\xa8\xd6\x28\x6d\xaf\xd2\xee\x74\x1a\x9d\xc6\xba\xe3\x2b\xe5\x4c\x69\xb6\x8e\x31\xc1\x46\x42\x79\xc3\x57\xca\xca\x37\x2f\xfe\x51\xae\x31\x92\x54\x4f\xfa\x96\x8a\x49\xbb\xbb\x66\xdf\x5d\xad\x1e\x7c\x4c\x83\x9d\x33\xdd\x66\xc3\xdd\x87\xcf\xe2\xe1\x20\x8b\x8f\xc2\xe0\xec\xf8\xf2\xe3\xfb\x93\x47\xdc\x3e\xa2\x0f\xdd\x6b\xe7\xfc\x0f\xb6\xda\xb7\xc0\x97\x42\x29\x21\x69\x44\x79\xdf\x22\x5c\xf0\x49\x22\x32\x65\x81\xf3\xff\xe7\x61\xee\xc5\x88\x68\x3f\x2e\x5d\x4b\x49\x8a\x72\xe6\xdc\xcb\xdc\x3a\x19\x1f\xd3\x83\xab\xf3\x07\xd1\xdc\xbd\xde\xb9\xbe\x8c\xe4\xe1\x71\xbb\x73\x79\x7b\x3a\x49\x86\x61\x76\xbf\x91\x8d\x3f\x27\x1e\x7b\x37\xbc\xf9\x94\x34\xff\x3b\x6e\x85\x82\x6b\x9b\x8c\x50\x89\x04\x9d\xd5\xc6\x7a\xa3\x99\xc7\xac\x4e\x7e\x99\x67\x78\x27\xe5\x91\x3f\xda\xf5\x9d\x4e\xb6\x1b\xab\x40\xaf\xb5\xd4\x49\x5b\x9c\xbd\xbb\xed\xac\xb5\xbf\x9c\x76\x98\xe0\xad\x68\xb2\x37\x7e\x38\xf9\x81\x67\x85\x6b\xb9\x43\x83\x72\x47\x4f\x04\x13\x78\x82\xdc\x28\x45\x1f\xb1\x07\xad\xb5\x74\xbc\x05\x29\x09\x02\xca\x23\x5b\x8b\xb4\x07\x9b\x4d\x43\xfa\x5a\x8a\x50\x78\x82\x84\xc8\x88\x72\x5b\xd2\x28\xd6\x3d\x68\x36\x56\x31\x99\x31\xbc\xd1\x98\xa4\x8c\x68\x54\xf0\x04\x01\x55\x29\x23\x93\x1e\x70\xc1\x71\xc6\xd3\xf0\x48\x10\x21\x3c\x81\x47\xfc\x87\x48\x8a\x8c\x07\xb6\x2f\x98\x90\x3d\x78\x83\x6d\xf3\x7f\x0b\xaa\x75\xb7\xdb\xdd\x9a\xb3\xb0\x6b\xcc\xc9\x09\x23\x2c\x2c\xf0\x04\x0b\xb6\x2a\xab\x72\x9b\x3b\x75\x93\x1b\x3e\x72\x8d\x12\x9e\x40\xe3\x58\xdb\x84\xd1\x88\xf7\xa0\x20\xd6\xb8\xee\xb3\xc4\x13\x5a\x0a\x0e\x71\xfb\x47\xbc\x42\xa7\x36\xd5\x98\xe4\x70\xe4\x25\xab\x07\xad\x46\xab\x8e\x43\x23\xf5\x88\x5c\xee\x62\xbb\xb5\xb9\x16\x76\xb6\x20\x2e\xed\x6f\x35\x9b\xbf\xd4\x05\x4d\x75\x22\x94\xe3\x72\x71\x2d\x09\x57\x29\x91\xc8\x75\x4d\x45\x1e\x36\xa1\xa8\xa6\x82\xf7\x80\x78\x4a\xb0\x4c\xe3\x16\x78\x42\x6b\x91\xf4\xa0\xb9\x05\x0c\x43\x9d\x3f\x54\x06\xe7\xdb\x3e\xda\x94\x07\x38\xee\xc1\xe6\xe6\x66\xcd\x08\xa5\x89\x46\x5b\xd1\x88\x63\x40\x39\xbc\x61\x22\xa2\xfc\x3b\x11\xad\xf3\x8b\x4c\xc3\x1b\x92\xa6\x8c\xfa\xc4\xd8\xf3\x2d\x31\xd7\x29\xb3\x71\x56\x45\x0f\xae\x4e\x4f\xba\xa0\x62\x9a\x00\xe1\x01\x5c\xa0\x4a\x05\x0f\x1a\xf7\x0a\x42\x21\xe1\x70\x6f\x03\x54\x96\x9a\xfe\x01\x22\x2c\x99\x91\x61\x82\x5c\xab\x5c\x20\xc1\x80\x12\xf8\x92\xa1\xa4\x58\xab\xe0\x46\xf5\xa7\xed\x8b\x0f\x87\x1f\xf6\x7b\x75\xa5\x81\x40\xc5\x7f\xd5\x30\x12\xf2\x01\x68\x08\x13\x91\x81\xe9\x50\x79\xe7\x48\x49\x84\x30\xa4\x04\x42\xca\xb0\xe7\x38\x73\xea\xfe\xa2\x21\x30\x0d\x87\x7b\xb0\xf9\x77\x75\x9c\x5c\xe5\x4b\x9a\x6a\x50\xd2\x7f\x71\x9d\x30\xdd\xb8\xab\x62\x3a\x74\x3a\x8d\xf5\x46\x67\xb6\xce\xab\xc3\xfd\x5c\x71\x58\x5e\x20\x3a\x47\x13\x67\xdf\xbb\x3c\x91\x51\x53\x6c\x4e\xba\x77\x5d\xde\xca\x46\xcd\x2f\xe3\xbb\xbd\x9d\x83\xf5\x9d\xb5\xb3\xf3\xf4\xe6\x5d\xf4\xe1\xfd\x2d\x69\x46\xdf\x2e\x10\x03\xd7\x29\x8c\xff\x37\xbe\xc8\x29\xb0\x4e\xab\xb1\xda\x68\x4f\x09\x2f\x75\x25\x5a\xa3\xdb\xe1\xf0\x2e\xfd\x83\xef\x9e\xb7\xaf\x82\xab\x0b\xe7\xe6\xe6\xf8\x28\xec\x78\xfb\x52\xac\x66\x5e\x37\x1c\x7e\xba\xfc\x74\x73\x41\xdb\x1f\xf6\x5e\xec\x8a\xfb\xfa\x2f\xe4\x01\x0d\xff\x2e\xa2\xe7\x3a\xd5\xb0\xe3\xe6\xd5\xcf\x67\x44\x29\x53\xe7\x4d\xfa\x3e\x3d\x99\x24\x68\x50\xc5\x44\x14\xe5\x79\xff\xf5\x6b\x75\x04\x9e\x9e\x00\x99\xc2\x29\x45\x64\xda\x90\x78\x00\x5f\xbf\x5a\x83\x95\x57\xaf\x5c\x4e\x86\x95\x3a\x4e\x86\xe6\xf4\x17\x3f\x76\x48\xc7\x18\x98\xc2\x54\x11\x02\x0c\xcd\x30\x91\x8b\xbd\x72\x03\x3a\x95\x9b\x9e\x7c\x3b\x64\x19\x0d\x0a\x86\x39\x8e\x52\x83\x71\x02\x65\xf9\xfe\x95\xeb\x65\x5a\x0b\x0e\x7a\x92\x62\xdf\x2a\x16\xd6\x82\x84\x16\x51\xc4\xd0\x94\x55\x46\x52\x85\x81\x05\x01\xd1\xa4\x24\x9b\x9d\x0b\x7a\x45\x26\x32\x32\x03\xe0\x1b\x4f\xd9\x38\x26\x49\xca\xd0\x2e\x15\x55\x9c\x76\xcb\x02\x22\x29\xb1\x71\x9c\x12\x1e\x60\xd0\xb7\x42\xc2\x14\x56\x46\xbd\x72\x55\x4a\xf8\x14\x60\x69\x0b\xce\x26\xd6\xe0\xaa\xb0\x83\x93\x21\x8d\xf2\xea\xe0\x3a\x86\x6f\xa9\x10\xf5\x05\xb7\x3d\x22\xf3\x98\xfe\x27\x98\x5c\xa7\x00\xab\x5a\x92\x05\xd0\x3c\x49\x78\x50\x75\xfe\x37\xd6\xc2\x0c\x48\xca\xf0\x38\x01\x1d\x0e\x56\x9e\x85\xaa\x02\x0a\x16\x80\xb3\x80\x06\x7d\xeb\xbb\xc0\x0e\x6a\x47\xc5\x0d\x85\x4c\x16\xec\xca\x49\xe5\xb3\x29\xeb\x73\x02\x00\x75\x2b\x0c\xab\x6d\x3a\x48\xba\xc0\x04\xe0\x52\x9e\x66\xba\xcc\x1a\xd3\xef\xac\x39\x21\x93\x8c\x52\x30\x0b\x52\x46\x7c\x8c\x05\x0b\x50\xf6\xad\xf7\x94\x69\x94\x85\x0f\x61\xf1\xbc\xb0\x79\x01\x47\x9d\x60\xd4\x55\x10\x67\xac\xe6\x4b\xe5\x43\xed\x31\x9f\x2a\x66\x39\xc4\xe8\xc0\x25\x0b\xa3\x57\x44\x75\x9c\x79\x79\xf1\x39\xc9\x1e\x69\x88\xd2\x19\x9a\xc0\xd8\xa6\x2d\x67\xd4\x1a\xb8\x74\xea\x08\x81\x90\xd8\x85\x40\x99\xae\x31\x0d\x02\xe4\x7d\x4b\xcb\x0c\x4d\x3a\xd0\x01\x5c\x8a\x4c\xfa\x08\x82\xc3\x7e\xce\x69\x42\xeb\x3a\x8c\x4e\xb3\x24\x63\xf5\x50\xe7\xed\xc4\x69\x2c\x84\x2d\x6f\x0f\x8b\x3c\x0b\x07\x3a\xe7\x59\x79\x1e\xa4\x59\xcb\xaf\x81\x39\xc7\x60\x72\x17\xf2\x76\x59\x5e\x98\x7a\xd0\xfc\x65\xab\x08\x83\xa6\x09\xe6\x99\x5d\x03\xbe\x5c\x18\x73\x38\x19\x56\x2d\xd6\x68\x34\x12\xb5\xe6\x6c\x2d\x35\xe8\x47\xf6\x48\x31\x9a\x4f\xd2\xf9\xbc\xb7\xc7\xca\x6e\xb5\x4d\xb1\xb1\x55\x62\x6f\x54\x0f\x22\x0c\x15\x6a\xbb\x78\x91\x04\xf6\x5a\xf5\x50\xbe\xe8\xe4\x6b\x16\x95\x2f\x58\x34\x7d\xf1\x9d\x04\x67\x54\xe9\x32\xc1\x73\xdf\x1e\x70\x62\x48\xcf\xb3\xdd\x44\xe4\xfd\xe1\xe7\xd3\x3d\x33\x02\x24\xe6\x02\xa9\x63\x3a\x1b\x12\xbe\x93\xbf\x73\x84\x65\x30\xaf\xcc\x9e\x8b\x7d\x9c\xb9\xf9\x67\x1a\xf5\x69\x00\xf2\x71\xea\x7f\x06\xfa\x6f\x62\x9f\x12\x8e\x0c\xf2\xbf\xb5\xa6\xb6\x80\xfb\x22\x7f\xde\xbc\x28\x8f\xac\xc1\x47\x86\x44\x21\x98\x86\x0a\x94\xbf\x7e\x86\xfc\x72\x71\xd3\xb7\x9f\xed\x02\xe0\xa6\xcf\x69\x00\xd7\x0a\xcb\x43\x0d\x24\xd3\x31\x72\x5d\x05\x45\x8b\x6a\x63\x2d\xcc\xe0\x27\xa1\xa8\xee\x94\x2b\x4d\xb8\x8f\xf9\x3c\x19\xa1\x06\xe2\xfb\xa8\x14\x54\x5c\x82\xa3\x6d\x4e\x1c\xa4\x44\xa9\x91\x90\x81\xea\x3d\xb7\xc5\x59\x62\x8c\x9b\x4e\xc3\x94\xdf\x27\x96\xf8\x00\x30\x2d\x76\x4f\x4f\xd0\x30\x16\x67\x92\x99\x09\xa3\x12\xf5\x34\x07\x4f\x73\x3b\x95\x34\x21\x72\xf2\x13\xb5\x8e\x46\xdc\xa6\x1c\x46\x54\xc7\xb5\x72\xf7\x12\x17\x96\xc4\xa7\x9e\xff\x4b\x99\x5e\x78\x5a\x8a\xeb\xc5\xf3\x73\x32\xbd\x4c\x4e\xc1\x9a\xbe\xaa\xee\x60\xf5\x63\x42\xa6\xad\xfa\x79\x75\x28\x2e\x6c\x4b\xa4\xcc\xb0\x5b\x1b\x15\xf2\xbb\xa9\x35\x68\xad\x76\xa1\xb3\xb6\x59\x8d\x0b\x35\xe6\x05\xc0\x1f\x70\x52\x40\xfb\x2d\x85\xf9\xa7\x22\x6b\x70\x29\x12\x84\x4b\xaa\x71\x51\x65\x0d\xfe\x1a\x5a\x73\x58\x19\x90\xee\xcf\x33\x94\x13\xf8\x8d\xa3\x49\x47\x22\x27\xf9\x95\x68\xfa\xd9\xe9\x57\x05\x47\x64\x48\x2e\x8b\xb1\x3d\x65\x59\x44\xb9\xfa\x7d\x76\x67\xf9\x89\x79\xfe\xde\xdc\xa3\x26\x4e\xab\xd1\x6a\x37\x56\xcb\xd5\xd2\x59\x7e\xc9\x67\x26\xa1\xf6\xbc\x8b\x13\xef\xc3\xf9\xe3\x49\xca\x8f\x0f\x1f\xf6\x02\xf9\x71\xb8\xce\xce\x26\x9b\x3b\xed\xf5\xf8\xe0\xfc\x8f\xcf\xe9\x06\x59\x3d\x1d\x6f\x9f\xff\x83\x49\xde\xb6\xe1\x90\xfb\x2c\x0b\x10\x08\x63\xe0\x8b\x24\xa5\x0c\x83\xca\x5b\xf8\xcd\x43\x26\x46\xbf\xff\x09\x42\x02\x2d\x19\x29\x0f\xe8\x90\x06\x19\x61\xf9\x3d\x4e\x01\x51\xc0\x11\x03\x0c\xfe\x15\x36\xdf\xfa\x30\x77\xaf\x16\x3e\x5d\xfd\x18\xab\xeb\xee\xdd\x1e\x1e\x87\xfb\x1f\xce\xee\x49\xb3\xb9\x7e\x7a\xba\xdb\xb9\x7d\x77\xd8\xdc\xee\x9c\x5d\xde\x9d\x9d\x7b\xb8\xbf\xf6\xd8\x0e\xdb\xb7\xcd\x38\xdb\x78\x39\x56\x3f\xe1\x92\xcf\x68\xea\x09\x22\xcb\x0b\xdc\x7a\xa3\x55\x23\xbd\xcc\x95\x5d\x12\x6e\xec\x67\x87\x6d\x3c\x39\x3e\x38\x62\x67\x9f\x4e\x2e\x2e\x9c\xc7\x8b\xc9\xe6\x0e\xfb\xe2\xdf\xaf\x5e\x5d\x5f\x66\x89\x37\xbe\x3d\xd8\x7c\xd8\x3f\xfc\x67\x61\xdf\xae\x75\x53\x95\xa2\x4f\x43\xea\xd7\xf3\x7d\x69\x28\x6b\x2d\xd8\xd8\x3e\xa7\xd7\x75\x4c\x1b\x19\xac\xb8\x4e\xf1\x59\xfc\xff\x02\x00\x00\xff\xff\x19\x2d\xb3\x3d\x27\x17\x00\x00") + +func indexHtmlBytes() ([]byte, error) { + return bindataRead( + _indexHtml, + "index.html", + ) +} + +func indexHtml() (*asset, error) { + bytes, err := indexHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "index.html", size: 5927, mode: os.FileMode(436), modTime: time.Unix(1497461475, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _applicationJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x56\x4d\x6f\xe3\x36\x10\xbd\xfb\x57\x0c\x36\x2e\x48\x35\x0a\xed\xec\xb6\x97\xa4\x3a\x14\x2e\x50\x2c\xb0\x45\x17\xd8\xdc\x8a\xb6\xa0\xa5\x91\xc5\x35\x4d\x0a\xe4\x38\xb6\xbb\xf0\x7f\x2f\xa8\x0f\x8b\xb2\x95\xbd\x18\x11\x39\x7c\x7c\xf3\xf8\x66\x26\x8b\x05\xfc\x8e\x06\x9d\x24\x2c\x60\x7d\x82\x95\x2d\x4b\xc4\x2f\xb9\x53\x35\xc1\xa3\x78\x7c\x2f\x7e\x9a\xf1\x72\x6f\x72\x52\xd6\xf0\x04\xbe\xcd\x00\x5e\xa5\x83\x5c\xab\x7a\x6d\xa5\x2b\x52\xc8\x1d\x4a\xc2\x3f\x5f\x3e\x7f\x24\xdc\xa5\x90\xef\x9d\x43\x43\x2f\x6a\x87\x76\x4f\x29\x14\xa8\xe5\x29\x85\x12\x29\xaf\x56\xb6\x40\x9f\x42\xa9\x34\xa1\x5b\x55\xd2\x6c\x30\x05\x65\x14\x29\xa9\xd5\x7f\xf8\x6b\x5d\x6b\x95\xcb\x70\x55\x0a\x0e\x4b\x87\xbe\x0a\x38\xee\xb3\xb3\x1b\x87\xde\xa7\x40\x2a\xdf\x86\xdf\x1d\x7e\xc2\x92\x52\xd8\xd7\x85\x24\x6c\x70\x9f\x67\x33\xb8\xba\x1d\x32\x58\xb6\xcb\x3d\x5d\xc8\xe0\xd5\xaa\xa2\x5b\x9e\x5f\xa7\x06\xa0\x4a\xe0\x73\xce\xd6\xb6\x38\xb1\x44\x54\xd2\xaf\xb4\xf4\x9e\x33\x4f\x92\xf0\xc1\xab\x8d\xc1\x42\x19\x96\xf4\xf1\x00\x0e\x69\xef\xcc\x74\x1a\x3c\x79\x6e\xa2\xce\x33\x80\x73\xd2\x52\x89\xe5\x82\x0c\x2e\x0c\x14\xe1\xae\x47\x0d\x12\x5b\xaa\x5b\x45\xa9\xd6\x2d\x0a\xd5\x1a\x32\x98\x73\x76\x67\xa9\x7e\x08\xf1\x81\x22\xed\x74\x7f\x4d\x77\xa4\x09\xa2\x5a\x8f\x57\x45\xa9\x4c\xc1\x99\x58\xcb\x62\x83\x2c\x11\x84\x47\x6a\x2e\x15\xb9\x2d\x50\x38\xac\xb5\xcc\x91\x2f\xfe\xe1\xe2\xdb\x87\x73\xd2\xfe\xce\x17\x29\xb0\xf9\x23\xcc\xdf\xb3\x64\x1a\x8e\x14\xe9\x31\x9c\x91\x3b\xec\x62\x3b\x6d\xfa\x23\xb2\xae\xd1\x14\x2f\x36\x28\x7c\xb7\xc5\x93\x56\x9e\x3a\xdc\x73\x23\x4e\x63\x96\x58\x94\x66\xe1\x8f\x2f\x98\x07\xdb\x6c\xa9\xd7\xa7\xc3\x3d\x28\x53\xd8\x83\xf0\xd8\x3f\x38\x2f\xb7\xbd\xe5\x9a\x43\x11\xf4\x60\xc0\x18\xff\x0a\x70\x2e\xe4\x57\x79\xe4\xfd\xd3\xee\x9d\x7e\x02\x16\xe4\xf1\xe2\xab\xb7\x86\xa5\xdd\x86\xdf\xe7\x39\x7a\xff\x14\xfb\xaf\xdf\x2b\x24\xc9\x97\x53\x8d\x4f\xc0\x9a\x33\xad\x01\x62\x26\x91\xfd\xa7\xb8\x84\xc7\x6f\x63\x5a\x11\xdb\xbf\xbb\x97\x6f\x3f\x58\x22\x5e\xa5\xe6\x89\x20\xfb\xc9\x1e\xd0\xad\xa4\x47\x3e\xd6\x7c\xce\x99\x88\x6c\x82\x32\xaf\x06\xb7\xab\xe2\x98\x02\xea\xc1\xc4\xad\xed\x51\x27\x93\xcf\x7a\x75\x8f\xd8\x49\x0a\x68\x0d\x95\x04\xb2\x2c\x03\xb3\xd7\x11\x5a\x44\x22\x40\x56\xaa\xb8\xb0\x03\x38\x03\x6a\x8f\x6f\xc5\xfa\xca\x1e\xa2\xd8\x1b\xf1\x26\xcb\x6c\x4a\xc5\x91\xc5\x04\xee\x6a\x3a\xf5\xb8\x23\x1d\xd7\x4d\xbe\x5b\x3c\xed\x6b\x36\x6e\x4d\x5d\x74\x68\x39\xfc\xe7\xe5\x72\xba\x25\x8d\x45\x1f\x4c\xc6\x23\xce\x53\xe7\xde\x7a\x78\x8f\xb9\x35\x85\x0f\xdd\xad\x45\x8e\x16\x20\xbb\x74\xbe\xdb\xc7\xbe\x0b\x5b\x21\xa1\x3c\xb4\xab\x83\x2a\xa8\x62\x29\xf0\xf8\xf8\x02\x3e\x2c\xe1\x47\x78\x5c\x2e\x13\xb8\x87\x77\x3f\xbc\x8b\x38\x86\x2c\x6f\x0a\xef\xfb\x35\xf7\xd1\x10\xba\x60\xc3\xa1\xe8\x46\x80\x2d\xd3\xb7\x12\x35\xf6\xd0\xa6\x60\xec\x01\x32\x30\x78\x80\xdf\x24\x05\x77\x6d\xda\x72\xbe\x4a\x91\x5f\xb5\xf6\x87\x70\x30\x81\x45\x48\x67\x39\x5c\x1b\x15\xe4\x28\x1d\x49\x32\xbe\x5d\xa5\xa0\xb1\x9d\x32\x29\x90\xdd\xa2\x69\x2f\xbb\x99\x1f\x17\x5e\x01\x41\x18\x3c\xd2\xbf\x07\x27\xeb\x1b\x96\xa1\x80\x2e\x53\x66\xa8\x84\xcb\x92\x28\xd0\x93\xb3\xa7\x78\x22\x7c\xd7\xa4\x0e\x4b\xc8\x9a\x66\x22\x1a\x7e\xbe\x6b\x06\xd6\x01\x57\x61\xb0\x35\x19\x40\x16\x02\x85\x46\xb3\xa1\xea\x19\x14\xfc\x12\x56\x9f\x41\xdd\xdf\x0f\x24\x9a\xf3\x6d\xe4\x5f\xea\xef\xbe\xb6\x46\x83\x88\x37\x31\x23\x6e\xf1\xcc\x0c\x2a\xac\xfa\xef\xb8\xb1\xa4\xc3\x25\x78\xa4\xa7\x41\x70\x72\x6a\xb3\x09\xdd\x61\xa2\xce\xfb\xbd\xc9\x89\x94\x5c\x26\x11\x03\x96\x02\x63\x93\xcd\x60\xdc\x45\xaf\xac\xd2\x58\x91\x0f\xa5\xd2\x7a\x7e\x19\xff\x13\x72\x31\xea\x39\x11\xb9\xd4\x9a\x53\xa5\xc2\xe2\xff\x01\x00\x00\xff\xff\x82\x35\x44\x82\x10\x09\x00\x00") + +func applicationJsBytes() ([]byte, error) { + return bindataRead( + _applicationJs, + "application.js", + ) +} + +func applicationJs() (*asset, error) { + bytes, err := applicationJsBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "application.js", size: 2320, mode: os.FileMode(436), modTime: time.Unix(1497465586, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "index.html": indexHtml, + "application.js": applicationJs, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "application.js": &bintree{applicationJs, map[string]*bintree{}}, + "index.html": &bintree{indexHtml, map[string]*bintree{}}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000..9171484 --- /dev/null +++ b/index.html @@ -0,0 +1,130 @@ + + + + + + + + Vault OTP-UI + + + + + + + + + + + + + + + +
+ +
+
+
+
+ +
+
+
+
+ +
+ +
+ +
+
+
+ +
+
Please sign in!
+
+

+ Use Github authentication to sign into your Vault instance and get access to your one-time passwords: +

+

+ Sign-in with Github +

+
+
+ +
+
+
+ +
+ + + + + + + + + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..4d86bf5 --- /dev/null +++ b/main.go @@ -0,0 +1,209 @@ +package main + +//go:generate go-bindata -pkg $GOPACKAGE -o assets.go index.html application.js + +import ( + "bytes" + "crypto/sha1" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "time" + + "github.com/Luzifer/rconfig" + log "github.com/Sirupsen/logrus" + "github.com/alecthomas/template" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + "github.com/tdewolff/minify" + "github.com/tdewolff/minify/html" + "github.com/tdewolff/minify/js" + validator "gopkg.in/validator.v2" +) + +const ( + requiredScope = "read:org,user" + sessionName = "vault-otp-ui" +) + +var ( + cfg struct { + Github struct { + ClientID string `flag:"client-id" default:"" env:"CLIENT_ID" description:"Github oAuth2 application Client ID" validate:"nonzero"` + ClientSecret string `flag:"client-secret" default:"" env:"CLIENT_SECRET" description:"Github oAuth2 application Client Secret" validate:"nonzero"` + } + Listen string `flag:"listen" default:":3000" description:"IP/Port to listen on"` + LogLevel string `flag:"log-level" default:"info" description:"Set log level (debug, info, warning, error)"` + SessionSecret string `flag:"session-secret" default:"" env:"SESSION_SECRET" description:"Secret to encrypt the session with"` + Vault struct { + Address string `flag:"vault-addr" env:"VAULT_ADDR" default:"https://127.0.0.1:8200" description:"Vault API address"` + Prefix string `flag:"vault-prefix" env:"VAULT_PREFIX" default:"/totp" description:"Prefix to search for OTP secrets / tokens in"` + SecretField string `flag:"vault-secret-field" env:"VAULT_SECRET_FIELD" default:"secret" description:"Field to search the secret in"` + } + VersionAndExit bool `flag:"version" default:"false" description:"Print version information and exit"` + } + + version = "dev" + mini = minify.New() + cookieStore *sessions.CookieStore +) + +func loadConfig() error { + if err := rconfig.Parse(&cfg); err != nil { + return err + } + + if err := validator.Validate(cfg); err != nil { + return err + } + + if l, err := log.ParseLevel(cfg.LogLevel); err == nil { + log.SetLevel(l) + } else { + log.Fatalf("Invalid log level: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("vault-otp-ui %s\n", version) + os.Exit(0) + } + + if cfg.SessionSecret == "" { + cookieStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64), securecookie.GenerateRandomKey(32)) + } else { + cookieStore = sessions.NewCookieStore([]byte(cfg.SessionSecret), []byte(fmt.Sprintf("%x", sha1.Sum([]byte(cfg.SessionSecret)))[0:32])) + } + + mini.AddFunc("text/html", html.Minify) + mini.AddFunc("application/javascript", js.Minify) + + return nil +} + +func main() { + var err error + if err = loadConfig(); err != nil { + log.Fatalf("Unable to parse CLI parameters: %s", err) + } + + http.HandleFunc("/oauth2", handleOAuthCallback) + http.HandleFunc("/application.js", handleApplicationJS) + http.HandleFunc("/codes.json", handleCodesJSON) + http.HandleFunc("/", handleIndexPage) + log.Fatalf("HTTP server exitted: %s", http.ListenAndServe(cfg.Listen, nil)) +} + +func getFileContentFallback(filename string) (io.Reader, error) { + if f, err := os.Open(filename); err == nil { + defer f.Close() + buf := new(bytes.Buffer) + io.Copy(buf, f) + return buf, nil + } + + if b, err := Asset(filename); err == nil { + return bytes.NewReader(b), nil + } + + return nil, errors.New("No suitable index page found") +} + +func handleIndexPage(res http.ResponseWriter, r *http.Request) { + sess, _ := cookieStore.Get(r, sessionName) + _, hasAccessToken := sess.Values["access_token"] + + content, err := getFileContentFallback("index.html") + + if err != nil { + http.Error(res, "No suitable index page found", http.StatusInternalServerError) + } + + buf := new(bytes.Buffer) + io.Copy(buf, content) + + tpl, err := template.New("index").Parse(buf.String()) + if err != nil { + log.Errorf("Parsing index template failed: %s", err) + http.Error(res, "No suitable index page found", http.StatusInternalServerError) + return + } + + outbuf := new(bytes.Buffer) + tpl.Execute(outbuf, map[string]interface{}{ + "isloggedin": hasAccessToken, + "authurl": getAuthenticationURL(), + }) + + mini.Minify("text/html", res, outbuf) +} + +func handleApplicationJS(res http.ResponseWriter, r *http.Request) { + content, err := getFileContentFallback("application.js") + + if err != nil { + http.Error(res, "No suitable file found", http.StatusInternalServerError) + } + + mini.Minify("application/javascript", res, content) +} + +func handleOAuthCallback(res http.ResponseWriter, r *http.Request) { + sess, _ := cookieStore.Get(r, sessionName) + + accessToken, err := getAccessToken(r.URL.Query().Get("code")) + if err != nil { + log.Errorf("An error occurred while fetching the access token: %s", err) + http.Error(res, "Something went wrong when fetching your access token. Sorry.", http.StatusInternalServerError) + return + } + + if accessToken == "" { + log.Errorf("Code %q was not resolved to an access token", r.URL.Query().Get("code")) + http.Error(res, "Something went wrong when fetching your access token. Sorry.", http.StatusInternalServerError) + return + } + + sess.Values["access_token"] = accessToken + if err := sess.Save(r, res); err != nil { + log.Errorf("Was not able to set the cookie: %s", err) + http.Error(res, "Something went wrong when fetching your access token. Sorry.", http.StatusInternalServerError) + return + } + + http.Redirect(res, r, "/", http.StatusFound) +} + +func handleCodesJSON(res http.ResponseWriter, r *http.Request) { + sess, _ := cookieStore.Get(r, sessionName) + iAccessToken, hasAccessToken := sess.Values["access_token"] + + if !hasAccessToken { + http.Error(res, `{"error":"Not logged in"}`, http.StatusUnauthorized) + return + } + + accessToken := iAccessToken.(string) + + tokens, err := getSecretsFromVault(accessToken) + if err != nil { + log.Errorf("Unable to fetch codes: %s", err) + http.Error(res, `{"error":"Unexpected error while fetching tokens"}`, http.StatusInternalServerError) + return + } + + n := time.Now() + result := struct { + Tokens []*token `json:"tokens"` + NextWrap time.Time `json:"next_wrap"` + }{ + Tokens: tokens, + NextWrap: n.Add(time.Duration(30-(n.Second()%30)) * time.Second), + } + + res.Header().Set("Content-Type", "application/json") + res.Header().Set("Cache-Control", "no-cache") + json.NewEncoder(res).Encode(result) +} diff --git a/oauth.go b/oauth.go new file mode 100644 index 0000000..be33017 --- /dev/null +++ b/oauth.go @@ -0,0 +1,34 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +func getAuthenticationURL() string { + return fmt.Sprintf("http://github.com/login/oauth/authorize?client_id=%s&scope=read:org user", cfg.Github.ClientID) +} + +func getAccessToken(code string) (string, error) { + v := url.Values{} + + v.Set("client_id", cfg.Github.ClientID) + v.Set("client_secret", cfg.Github.ClientSecret) + v.Set("code", code) + + buf := bytes.NewReader([]byte(v.Encode())) + req, _ := http.NewRequest("POST", "https://github.com/login/oauth/access_token", buf) + req.Header.Set("Accept", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + r := map[string]string{} + return r["access_token"], json.NewDecoder(resp.Body).Decode(&r) +} diff --git a/token.go b/token.go new file mode 100644 index 0000000..d05b2d6 --- /dev/null +++ b/token.go @@ -0,0 +1,166 @@ +package main + +import ( + "fmt" + "path" + "sort" + "strings" + "sync" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/builtin/credential/github" + "github.com/pquerna/otp/totp" + "github.com/prometheus/common/log" +) + +type token struct { + Name string `json:"name"` + Secret string `json:"-"` + Code string `json:"code"` +} + +func (t *token) GenerateCode(in time.Time) error { + secret := t.Secret + + if n := len(secret) % 8; n != 0 { + secret = secret + strings.Repeat("=", 8-n) + } + + var err error + t.Code, err = totp.GenerateCode(strings.ToUpper(secret), in) + return err +} + +// Sorter interface + +type tokenList []*token + +func (t tokenList) Len() int { return len(t) } +func (t tokenList) Less(i, j int) bool { return strings.ToLower(t[i].Name) < strings.ToLower(t[j].Name) } +func (t tokenList) Swap(i, j int) { t[i], t[j] = t[j], t[i] } + +func (t tokenList) LongestName() (l int) { + for _, s := range t { + if ll := len(s.Name); ll > l { + l = ll + } + } + + return +} + +func getSecretsFromVault(accessToken string) ([]*token, error) { + client, err := api.NewClient(&api.Config{ + Address: cfg.Vault.Address, + }) + + if err != nil { + return nil, fmt.Errorf("Unable to create client: %s", err) + } + + handler := &github.CLIHandler{} + t, err := handler.Auth(client, map[string]string{"token": accessToken}) + if err != nil { + return nil, err + } + + client.SetToken(t) + + key := cfg.Vault.Prefix + + resp := []*token{} + respChan := make(chan *token, 100) + + keyPoolChan := make(chan string, 100) + + scanPool := make(chan string, 100) + scanPool <- strings.TrimRight(key, "*") + + done := make(chan struct{}) + defer func() { done <- struct{}{} }() + + wg := new(sync.WaitGroup) + wg.Add(1) + + go func() { + for { + select { + case key := <-scanPool: + go scanKeyForSubKeys(client, key, scanPool, keyPoolChan, wg) + case key := <-keyPoolChan: + go fetchTokenFromKey(client, key, respChan, wg) + case t := <-respChan: + resp = append(resp, t) + case <-done: + close(scanPool) + close(keyPoolChan) + close(respChan) + return + } + } + }() + + wg.Wait() + + sort.Sort(tokenList(resp)) + + return resp, nil +} + +func scanKeyForSubKeys(client *api.Client, key string, subKeyChan, tokenKeyChan chan string, wg *sync.WaitGroup) { + defer wg.Done() + + s, err := client.Logical().List(key) + if err != nil { + log.Errorf("Unable to list keys %q: %s", key, err) + return + } + + if s == nil { + log.Errorf("There is no key %q", key) + return + } + + if s.Data["keys"] != nil { + for _, sk := range s.Data["keys"].([]interface{}) { + sks := sk.(string) + if strings.HasSuffix(sks, "/") { + wg.Add(1) + subKeyChan <- path.Join(key, sks) + } else { + wg.Add(1) + tokenKeyChan <- path.Join(key, sks) + } + } + } +} + +func fetchTokenFromKey(client *api.Client, k string, respChan chan *token, wg *sync.WaitGroup) { + defer wg.Done() + + data, err := client.Logical().Read(k) + if err != nil { + log.Errorf("Unable to read from key %q: %s", k, err) + return + } + + tok := &token{} + + if data.Data[cfg.Vault.SecretField] != nil { + tok.Secret = data.Data[cfg.Vault.SecretField].(string) + tok.GenerateCode(time.Now()) + } else if data.Data["code"] != nil { + tok.Code = data.Data["code"].(string) + } else { + // Secret did not have our field or a code, looks bad + return + } + + tok.Name = k + if data.Data["name"] != nil { + tok.Name = data.Data["name"].(string) + } + + respChan <- tok +}