From 154562e9612542544c709ab4f2260f4771f5ba22 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 14 Jun 2017 20:40:12 +0200 Subject: [PATCH] Initial version Signed-off-by: Knut Ahlers --- .gitignore | 2 + Makefile | 6 ++ application.coffee | 69 ++++++++++++ application.js | 92 ++++++++++++++++ assets.go | 258 +++++++++++++++++++++++++++++++++++++++++++++ index.html | 130 +++++++++++++++++++++++ main.go | 209 ++++++++++++++++++++++++++++++++++++ oauth.go | 34 ++++++ token.go | 166 +++++++++++++++++++++++++++++ 9 files changed, 966 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 application.coffee create mode 100644 application.js create mode 100644 assets.go create mode 100644 index.html create mode 100644 main.go create mode 100644 oauth.go create mode 100644 token.go 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 +}