@standardnotes/authenticator 2.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +21 -0
- package/.browserlistrc +2 -0
- package/.eslintignore +3 -0
- package/.eslintrc +30 -0
- package/CHANGELOG.md +136 -0
- package/README.md +7 -0
- package/app/assets/svg/drag-indicator.svg +14 -0
- package/app/assets/svg/palette.svg +3 -0
- package/app/assets/svg/reorder-icon.svg +37 -0
- package/app/components/AuthEntry.jsx +202 -0
- package/app/components/AuthMenu.jsx +63 -0
- package/app/components/ConfirmDialog.jsx +37 -0
- package/app/components/CopyNotification.jsx +15 -0
- package/app/components/CountdownPie.jsx +131 -0
- package/app/components/DataErrorAlert.jsx +21 -0
- package/app/components/EditEntry.jsx +283 -0
- package/app/components/Home.jsx +365 -0
- package/app/components/QRCodeReader.jsx +91 -0
- package/app/components/ViewEntries.jsx +82 -0
- package/app/index.js +4 -0
- package/app/lib/otp.js +184 -0
- package/app/lib/utils.js +185 -0
- package/app/stylesheets/main.scss +443 -0
- package/editor.index.ejs +10 -0
- package/ext.json.sample +8 -0
- package/package.json +68 -0
- package/webpack.config.js +74 -0
- package/webpack.dev.js +23 -0
- package/webpack.prod.js +11 -0
package/app/lib/otp.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { base32ToHex, bufToHex, decToHex, hextoBuf, hexToBytes, leftpad } from '@Lib/utils'
|
|
2
|
+
export { parseKeyUri, secretPattern } from '@Lib/utils'
|
|
3
|
+
|
|
4
|
+
class Hotp {
|
|
5
|
+
/**
|
|
6
|
+
* Generate a counter based One Time Password
|
|
7
|
+
*
|
|
8
|
+
* @return {String} the one time password
|
|
9
|
+
*
|
|
10
|
+
* Arguments:
|
|
11
|
+
*
|
|
12
|
+
* args
|
|
13
|
+
* key - Key for the one time password. This should be unique and secret for
|
|
14
|
+
* every user as this is the seed that is used to calculate the HMAC
|
|
15
|
+
*
|
|
16
|
+
* counter - Counter value. This should be stored by the application, must
|
|
17
|
+
* be user specific, and be incremented for each request.
|
|
18
|
+
*
|
|
19
|
+
*/
|
|
20
|
+
async gen(secret, opt) {
|
|
21
|
+
var key = base32ToHex(secret) || ''
|
|
22
|
+
opt = opt || {}
|
|
23
|
+
var counter = opt.counter || 0
|
|
24
|
+
|
|
25
|
+
var hexCounter = leftpad(decToHex(counter), 16, '0')
|
|
26
|
+
var digest = await this.createHmac('SHA-1', key, hexCounter)
|
|
27
|
+
var h = hexToBytes(digest)
|
|
28
|
+
|
|
29
|
+
// Truncate
|
|
30
|
+
var offset = h[h.length - 1] & 0xf
|
|
31
|
+
var v =
|
|
32
|
+
((h[offset] & 0x7f) << 24) |
|
|
33
|
+
((h[offset + 1] & 0xff) << 16) |
|
|
34
|
+
((h[offset + 2] & 0xff) << 8) |
|
|
35
|
+
(h[offset + 3] & 0xff)
|
|
36
|
+
|
|
37
|
+
v = (v % 1000000) + ''
|
|
38
|
+
|
|
39
|
+
return Array(7 - v.length).join('0') + v
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check a One Time Password based on a counter.
|
|
44
|
+
*
|
|
45
|
+
* @return {Object} null if failure, { delta: # } on success
|
|
46
|
+
* delta is the time step difference between the client and the server
|
|
47
|
+
*
|
|
48
|
+
* Arguments:
|
|
49
|
+
*
|
|
50
|
+
* args
|
|
51
|
+
* key - Key for the one time password. This should be unique and secret for
|
|
52
|
+
* every user as it is the seed used to calculate the HMAC
|
|
53
|
+
*
|
|
54
|
+
* token - Passcode to validate.
|
|
55
|
+
*
|
|
56
|
+
* window - The allowable margin for the counter. The function will check
|
|
57
|
+
* 'W' codes in the future against the provided passcode. Note,
|
|
58
|
+
* it is the calling applications responsibility to keep track of
|
|
59
|
+
* 'W' and increment it for each password check, and also to adjust
|
|
60
|
+
* it accordingly in the case where the client and server become
|
|
61
|
+
* out of sync (second argument returns non zero).
|
|
62
|
+
* E.g. if W = 100, and C = 5, this function will check the passcode
|
|
63
|
+
* against all One Time Passcodes between 5 and 105.
|
|
64
|
+
*
|
|
65
|
+
* Default - 50
|
|
66
|
+
*
|
|
67
|
+
* counter - Counter value. This should be stored by the application, must
|
|
68
|
+
* be user specific, and be incremented for each request.
|
|
69
|
+
*
|
|
70
|
+
*/
|
|
71
|
+
async verify(token, key, opt) {
|
|
72
|
+
opt = opt || {}
|
|
73
|
+
var window = opt.window || 50
|
|
74
|
+
var counter = opt.counter || 0
|
|
75
|
+
|
|
76
|
+
// Now loop through from C to C + W to determine if there is
|
|
77
|
+
// a correct code
|
|
78
|
+
for (var i = counter - window; i <= counter + window; ++i) {
|
|
79
|
+
opt.counter = i
|
|
80
|
+
if ((await this.gen(key, opt)) === token) {
|
|
81
|
+
// We have found a matching code, trigger callback
|
|
82
|
+
// and pass offset
|
|
83
|
+
return { delta: i - counter }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If we get to here then no codes have matched, return null
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async createHmac(alg, key, str) {
|
|
92
|
+
const hmacKey = await window.crypto.subtle.importKey(
|
|
93
|
+
'raw', // raw format of the key - should be Uint8Array
|
|
94
|
+
hextoBuf(key),
|
|
95
|
+
{
|
|
96
|
+
// algorithm details
|
|
97
|
+
name: 'HMAC',
|
|
98
|
+
hash: { name: alg },
|
|
99
|
+
},
|
|
100
|
+
false, // export = false
|
|
101
|
+
['sign'], // what this key can do
|
|
102
|
+
)
|
|
103
|
+
const sig = await window.crypto.subtle.sign('HMAC', hmacKey, hextoBuf(str))
|
|
104
|
+
return bufToHex(sig)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const hotp = new Hotp()
|
|
109
|
+
|
|
110
|
+
class Totp {
|
|
111
|
+
/**
|
|
112
|
+
* Generate a time based One Time Password
|
|
113
|
+
*
|
|
114
|
+
* @return {String} the one time password
|
|
115
|
+
*
|
|
116
|
+
* Arguments:
|
|
117
|
+
*
|
|
118
|
+
* args
|
|
119
|
+
* key - Key for the one time password. This should be unique and secret for
|
|
120
|
+
* every user as it is the seed used to calculate the HMAC
|
|
121
|
+
*
|
|
122
|
+
* time - The time step of the counter. This must be the same for
|
|
123
|
+
* every request and is used to calculat C.
|
|
124
|
+
*
|
|
125
|
+
* Default - 30
|
|
126
|
+
*
|
|
127
|
+
*/
|
|
128
|
+
async gen(key, opt) {
|
|
129
|
+
opt = opt || {}
|
|
130
|
+
var time = opt.time || 30
|
|
131
|
+
var _t = Date.now()
|
|
132
|
+
|
|
133
|
+
// Determine the value of the counter, C
|
|
134
|
+
// This is the number of time steps in seconds since T0
|
|
135
|
+
opt.counter = Math.floor(_t / 1000 / time)
|
|
136
|
+
|
|
137
|
+
return hotp.gen(key, opt)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check a One Time Password based on a timer.
|
|
142
|
+
*
|
|
143
|
+
* @return {Object} null if failure, { delta: # } on success
|
|
144
|
+
* delta is the time step difference between the client and the server
|
|
145
|
+
*
|
|
146
|
+
* Arguments:
|
|
147
|
+
*
|
|
148
|
+
* args
|
|
149
|
+
* key - Key for the one time password. This should be unique and secret for
|
|
150
|
+
* every user as it is the seed used to calculate the HMAC
|
|
151
|
+
*
|
|
152
|
+
* token - Passcode to validate.
|
|
153
|
+
*
|
|
154
|
+
* window - The allowable margin for the counter. The function will check
|
|
155
|
+
* 'W' codes either side of the provided counter. Note,
|
|
156
|
+
* it is the calling applications responsibility to keep track of
|
|
157
|
+
* 'W' and increment it for each password check, and also to adjust
|
|
158
|
+
* it accordingly in the case where the client and server become
|
|
159
|
+
* out of sync (second argument returns non zero).
|
|
160
|
+
* E.g. if W = 5, and C = 1000, this function will check the passcode
|
|
161
|
+
* against all One Time Passcodes between 995 and 1005.
|
|
162
|
+
*
|
|
163
|
+
* Default - 6
|
|
164
|
+
*
|
|
165
|
+
* time - The time step of the counter. This must be the same for
|
|
166
|
+
* every request and is used to calculate C.
|
|
167
|
+
*
|
|
168
|
+
* Default - 30
|
|
169
|
+
*
|
|
170
|
+
*/
|
|
171
|
+
async verify(token, key, opt) {
|
|
172
|
+
opt = opt || {}
|
|
173
|
+
var time = opt.time || 30
|
|
174
|
+
var _t = Date.now()
|
|
175
|
+
|
|
176
|
+
// Determine the value of the counter, C
|
|
177
|
+
// This is the number of time steps in seconds since T0
|
|
178
|
+
opt.counter = Math.floor(_t / 1000 / time)
|
|
179
|
+
|
|
180
|
+
return hotp.verify(token, key, opt)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const totp = new Totp()
|
package/app/lib/utils.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
|
|
2
|
+
export const secretPattern = `^[${base32chars}]{16,}$`
|
|
3
|
+
|
|
4
|
+
export function hexToBytes(hex) {
|
|
5
|
+
var bytes = []
|
|
6
|
+
for (var c = 0, C = hex.length; c < C; c += 2) {
|
|
7
|
+
bytes.push(parseInt(hex.substr(c, 2), 16))
|
|
8
|
+
}
|
|
9
|
+
return bytes
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decToHex(s) {
|
|
13
|
+
return (s < 15.5 ? '0' : '') + Math.round(s).toString(16)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function bufToHex(buf) {
|
|
17
|
+
return Array.prototype.map.call(new Uint8Array(buf), (x) => ('00' + x.toString(16)).slice(-2)).join('')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hextoBuf(hex) {
|
|
21
|
+
var view = new Uint8Array(hex.length / 2)
|
|
22
|
+
|
|
23
|
+
for (var i = 0; i < hex.length; i += 2) {
|
|
24
|
+
view[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return view.buffer
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function base32ToHex(base32) {
|
|
31
|
+
var bits, chunk, hex, i, val
|
|
32
|
+
bits = ''
|
|
33
|
+
hex = ''
|
|
34
|
+
i = 0
|
|
35
|
+
while (i < base32.length) {
|
|
36
|
+
val = base32chars.indexOf(base32.charAt(i).toUpperCase())
|
|
37
|
+
bits += leftpad(val.toString(2), 5, '0')
|
|
38
|
+
i++
|
|
39
|
+
}
|
|
40
|
+
i = 0
|
|
41
|
+
while (i + 4 <= bits.length) {
|
|
42
|
+
chunk = bits.substr(i, 4)
|
|
43
|
+
hex = hex + parseInt(chunk, 2).toString(16)
|
|
44
|
+
i += 4
|
|
45
|
+
}
|
|
46
|
+
return hex
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function leftpad(str, len, pad) {
|
|
50
|
+
if (len + 1 >= str.length) {
|
|
51
|
+
str = Array(len + 1 - str.length).join(pad) + str
|
|
52
|
+
}
|
|
53
|
+
return str
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* This function takes an otpauth:// style key URI and parses it into an object with keys for the
|
|
58
|
+
* various parts of the URI
|
|
59
|
+
*
|
|
60
|
+
* @param {String} uri The otpauth:// uri that you want to parse
|
|
61
|
+
*
|
|
62
|
+
* @return {Object} The parsed URI or null on failure. The URI object looks like this:
|
|
63
|
+
*
|
|
64
|
+
* {
|
|
65
|
+
* type: 'totp',
|
|
66
|
+
* label: { issuer: 'ACME Co', account: 'jane@example.com' },
|
|
67
|
+
* query: {
|
|
68
|
+
* secret: 'JBSWY3DPEHPK3PXP',
|
|
69
|
+
* digits: '6'
|
|
70
|
+
* }
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* @see <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">otpauth Key URI Format</a>
|
|
74
|
+
*/
|
|
75
|
+
export function parseKeyUri(uri) {
|
|
76
|
+
// Quick sanity check
|
|
77
|
+
if (typeof uri !== 'string' || uri.length < 7) return null
|
|
78
|
+
|
|
79
|
+
// I would like to just use new URL(), but the behavior is different between node and browsers, so
|
|
80
|
+
// we have to do some of the work manually with regex.
|
|
81
|
+
const parts = /otpauth:\/\/([A-Za-z]+)\/([^?]+)\??(.*)?/i.exec(uri)
|
|
82
|
+
|
|
83
|
+
if (!parts || parts.length < 3) {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// eslint-disable-next-line no-unused-vars
|
|
88
|
+
const [fullUri, type, fullLabel] = parts
|
|
89
|
+
|
|
90
|
+
// Sanity check type and label
|
|
91
|
+
if (!type || !fullLabel) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Parse the label
|
|
96
|
+
const decodedLabel = decodeURIComponent(fullLabel)
|
|
97
|
+
|
|
98
|
+
const labelParts = decodedLabel.split(/: ?/)
|
|
99
|
+
|
|
100
|
+
const label =
|
|
101
|
+
labelParts && labelParts.length === 2
|
|
102
|
+
? { issuer: labelParts[0], account: labelParts[1] }
|
|
103
|
+
: { issuer: '', account: decodedLabel }
|
|
104
|
+
|
|
105
|
+
// Parse query string
|
|
106
|
+
const qs = parts[3] ? new URLSearchParams(parts[3]) : []
|
|
107
|
+
|
|
108
|
+
const query = [...qs].reduce((acc, [key, value]) => {
|
|
109
|
+
acc[key] = value
|
|
110
|
+
|
|
111
|
+
return acc
|
|
112
|
+
}, {})
|
|
113
|
+
|
|
114
|
+
// Returned the parsed parts of the URI
|
|
115
|
+
return { type: type.toLowerCase(), label, query }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Converts a hex color string to an object containing RGB values.
|
|
120
|
+
*/
|
|
121
|
+
export function hexColorToRGB(hexColor) {
|
|
122
|
+
// Expand the shorthand form (e.g. "0AB") to full form (e.g. "00AABB")
|
|
123
|
+
const shortHandFormRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
|
124
|
+
hexColor = hexColor.replace(shortHandFormRegex, function (m, red, green, blue) {
|
|
125
|
+
return red + red + green + green + blue + blue
|
|
126
|
+
})
|
|
127
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor)
|
|
128
|
+
return result
|
|
129
|
+
? {
|
|
130
|
+
red: parseInt(result[1], 16),
|
|
131
|
+
green: parseInt(result[2], 16),
|
|
132
|
+
blue: parseInt(result[3], 16),
|
|
133
|
+
}
|
|
134
|
+
: null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const defaultBgColor = '#FFF'
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Gets the color variable to be used based on the calculated constrast of a color.
|
|
141
|
+
*/
|
|
142
|
+
export function getVarColorForContrast(backgroundColor) {
|
|
143
|
+
const styleKitColors = {
|
|
144
|
+
foreground: '--sn-stylekit-contrast-foreground-color',
|
|
145
|
+
background: '--sn-stylekit-contrast-background-color',
|
|
146
|
+
}
|
|
147
|
+
if (!backgroundColor) {
|
|
148
|
+
return styleKitColors.foreground
|
|
149
|
+
}
|
|
150
|
+
const colorContrast = Math.round(
|
|
151
|
+
(parseInt(backgroundColor.red) * 299 +
|
|
152
|
+
parseInt(backgroundColor.green) * 587 +
|
|
153
|
+
parseInt(backgroundColor.blue) * 114) /
|
|
154
|
+
1000,
|
|
155
|
+
)
|
|
156
|
+
return colorContrast > 70 ? styleKitColors.background : styleKitColors.foreground
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function getPropertyValue(document, propertyName) {
|
|
160
|
+
return getComputedStyle(document.documentElement).getPropertyValue(propertyName).trim().toUpperCase()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const contextualColors = ['info', 'success', 'neutral', 'warning']
|
|
164
|
+
|
|
165
|
+
export function getContextualColor(document, colorName) {
|
|
166
|
+
if (!contextualColors.includes(colorName)) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return getPropertyValue(document, `--sn-stylekit-${colorName}-color`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function getEntryColor(document, entry) {
|
|
174
|
+
const { color } = entry
|
|
175
|
+
|
|
176
|
+
if (!contextualColors.includes(color)) {
|
|
177
|
+
return color
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return getContextualColor(document, color)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getAllContextualColors(document) {
|
|
184
|
+
return contextualColors.map((colorName) => getContextualColor(document, colorName))
|
|
185
|
+
}
|