From 614558e2b96def1eafff3c1a402efb154cfe8218 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 1 Jul 2020 16:54:52 +0200 Subject: [PATCH 01/81] Hotfix --- dist/chromium/manifest.json | 5 +- package-lock.json | 935 +++++++++++++++++++++++------------- package.json | 10 +- src/crypto.ts | 62 ++- src/storage.ts | 15 + src/utils.ts | 8 +- src/webauthn.ts | 44 +- 7 files changed, 702 insertions(+), 377 deletions(-) diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index 744560f..0f8b3cf 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -2,13 +2,14 @@ "manifest_version": 2, "name": "CKey", "description": "A Chrome Extension that emulates a Hardware Authentication Device", - "version": "1.0.2", + "version": "1.0.4", "minimum_chrome_version": "36.0.1985.18", "content_scripts": [ { "all_frames": true, "matches": [ - "https://*/*" + "https://*/*", + "http://localhost/*" ], "exclude_matches": [ "https://*/*.xml" diff --git a/package-lock.json b/package-lock.json index d3ae219..de08684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ckey", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -69,9 +69,9 @@ "dev": true }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "ms": { @@ -310,9 +310,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -818,178 +818,177 @@ "dev": true }, "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", + "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", "dev": true, "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", + "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", + "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", + "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", "dev": true }, "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", + "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", "dev": true, "requires": { - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", + "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", "dev": true }, "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", + "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" + "@webassemblyjs/ast": "1.9.0" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", + "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", + "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0" } }, "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", + "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", + "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", + "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", + "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/helper-wasm-section": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-opt": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "@webassemblyjs/wast-printer": "1.9.0" } }, "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", + "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", + "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-buffer": "1.9.0", + "@webassemblyjs/wasm-gen": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0" } }, "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", + "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-wasm-bytecode": "1.9.0", + "@webassemblyjs/ieee754": "1.9.0", + "@webassemblyjs/leb128": "1.9.0", + "@webassemblyjs/utf8": "1.9.0" } }, "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", + "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/floating-point-hex-parser": "1.9.0", + "@webassemblyjs/helper-api-error": "1.9.0", + "@webassemblyjs/helper-code-frame": "1.9.0", + "@webassemblyjs/helper-fsm": "1.9.0", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", + "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/wast-parser": "1.9.0", "@xtuc/long": "4.2.2" } }, @@ -1028,9 +1027,9 @@ }, "dependencies": { "acorn": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", - "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true } } @@ -1060,9 +1059,9 @@ "dev": true }, "ajv-keywords": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", - "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.0.tgz", + "integrity": "sha512-eyoaac3btgU8eJlvh01En8OCKzRqlLe2G5jDsCr3RiE2uLGMEEB1aaGwVVpwR8M95956tGH6R+9edC++OvzaVw==", "dev": true }, "ansi-colors": { @@ -1242,6 +1241,22 @@ "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } + } + }, + "asn1js": { + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-2.0.26.tgz", + "integrity": "sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ==", + "requires": { + "pvutils": "^1.0.17" } }, "assert": { @@ -1293,7 +1308,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true + "dev": true, + "optional": true }, "async-limiter": { "version": "1.0.1", @@ -2190,10 +2206,11 @@ "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" }, "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true, + "optional": true }, "bindings": { "version": "1.5.0", @@ -2247,10 +2264,9 @@ "dev": true }, "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", + "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==" }, "brace-expansion": { "version": "1.1.11", @@ -2283,8 +2299,7 @@ "brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, "browser-process-hrtime": { "version": "0.1.3", @@ -2360,21 +2375,56 @@ "requires": { "bn.js": "^4.1.0", "randombytes": "^2.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz", + "integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==", + "dev": true, + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.2", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "browserify-zlib": { @@ -2456,9 +2506,9 @@ "dev": true }, "cacache": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz", - "integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", + "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", "dev": true, "requires": { "bluebird": "^3.5.5", @@ -2569,37 +2619,90 @@ } }, "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz", + "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==", "dev": true, + "optional": true, "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.4.0" }, "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "optional": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "optional": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "optional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "optional": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true + "dev": true, + "optional": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "optional": true, + "requires": { + "is-number": "^7.0.0" + } } } }, "chownr": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, "chrome-trace-event": { @@ -2887,6 +2990,14 @@ "requires": { "bn.js": "^4.1.0", "elliptic": "^6.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "create-hash": { @@ -3078,9 +3189,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true } } @@ -3148,6 +3259,14 @@ "bn.js": "^4.1.0", "miller-rabin": "^4.0.0", "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "domain-browser": { @@ -3218,10 +3337,9 @@ "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", - "dev": true, + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "requires": { "bn.js": "^4.4.0", "brorand": "^1.0.1", @@ -3230,6 +3348,13 @@ "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } } }, "emoji-regex": { @@ -3591,9 +3716,9 @@ } }, "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", + "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", "dev": true }, "file-uri-to-path": { @@ -4349,24 +4474,13 @@ } }, "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, + "optional": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "is-glob": "^4.0.1" } }, "global-modules": { @@ -4390,9 +4504,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true } } @@ -4434,26 +4548,6 @@ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", "dev": true }, - "handlebars": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -4533,20 +4627,45 @@ } }, "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dev": true, "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "hash.js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, "requires": { "inherits": "^2.0.3", "minimalistic-assert": "^1.0.1" @@ -4562,7 +4681,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, "requires": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", @@ -4603,6 +4721,12 @@ "whatwg-encoding": "^1.0.1" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4730,8 +4854,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -4776,12 +4899,13 @@ "dev": true }, "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "optional": true, "requires": { - "binary-extensions": "^1.0.0" + "binary-extensions": "^2.0.0" } }, "is-buffer": { @@ -5085,12 +5209,12 @@ } }, "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", "dev": true, "requires": { - "handlebars": "^4.1.2" + "html-escaper": "^2.0.0" } }, "iterate-object": { @@ -5996,9 +6120,9 @@ } }, "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" }, "js-tokens": { "version": "3.0.2", @@ -6120,6 +6244,11 @@ "verror": "1.10.0" } }, + "jsrsasign": { + "version": "8.0.20", + "resolved": "https://registry.npmjs.org/jsrsasign/-/jsrsasign-8.0.20.tgz", + "integrity": "sha512-JTXt9+nqdynIB8wFsS6e8ffHhIjilhywXwdaEVHSj9OVmwldG2H0EoCqkQ+KXkm2tVqREfH/HEmklY4k1/6Rcg==" + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -6240,9 +6369,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -6367,12 +6496,6 @@ "tmpl": "1.0.x" } }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -6476,9 +6599,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true } } @@ -6491,6 +6614,14 @@ "requires": { "bn.js": "^4.0.0", "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "mime-db": { @@ -6517,14 +6648,12 @@ "minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, "minimalistic-crypto-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "minimatch": { "version": "3.0.4", @@ -6581,12 +6710,20 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + } } }, "mocha": { @@ -6648,6 +6785,15 @@ "path-exists": "^3.0.0" } }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -6686,6 +6832,16 @@ "requires": { "has-flag": "^3.0.0" } + }, + "yargs-parser": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, @@ -6755,9 +6911,9 @@ } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true } } @@ -7004,16 +7160,6 @@ "wrappy": "1" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, "optionator": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", @@ -7115,9 +7261,9 @@ "dev": true }, "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, "parallel-transform": { @@ -7183,7 +7329,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true + "dev": true, + "optional": true }, "path-exists": { "version": "3.0.0", @@ -7219,9 +7366,9 @@ } }, "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", + "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", "dev": true, "requires": { "create-hash": "^1.1.2", @@ -7237,6 +7384,13 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true, + "optional": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -7366,6 +7520,14 @@ "parse-asn1": "^5.0.0", "randombytes": "^2.0.1", "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", + "dev": true + } } }, "pump": { @@ -7407,6 +7569,11 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "pvutils": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.0.17.tgz", + "integrity": "sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ==" + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", @@ -7538,14 +7705,13 @@ } }, "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", "dev": true, + "optional": true, "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" + "picomatch": "^2.2.1" } }, "realpath-native": { @@ -7890,9 +8056,9 @@ }, "dependencies": { "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true } } @@ -7921,10 +8087,13 @@ "dev": true }, "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.1.0.tgz", + "integrity": "sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "set-blocking": { "version": "2.0.0", @@ -8477,9 +8646,9 @@ } }, "terser": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", - "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", + "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -8500,9 +8669,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -8512,16 +8681,16 @@ } }, "terser-webpack-plugin": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz", - "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.4.tgz", + "integrity": "sha512-U4mACBHIegmfoEe5fdongHESNJWqsGU+W0S/9+BmYGVQDw1+c2Ow05TpMhxjPK1sRb7cuYq1BPl1e5YHJMTCqA==", "dev": true, "requires": { "cacache": "^12.0.2", "find-cache-dir": "^2.1.0", "is-wsl": "^1.1.0", "schema-utils": "^1.0.0", - "serialize-javascript": "^2.1.2", + "serialize-javascript": "^3.1.0", "source-map": "^0.6.1", "terser": "^4.1.2", "webpack-sources": "^1.4.0", @@ -8569,9 +8738,9 @@ } }, "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -8781,9 +8950,9 @@ } }, "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "yargs-parser": { @@ -8975,33 +9144,6 @@ "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", "dev": true }, - "uglify-js": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.2.tgz", - "integrity": "sha512-uhRwZcANNWVLrxLfNFEdltoPNhECUR3lc+UdJoG9CBpMcSnKyWA94tc3eAujB1GcMY5Uwq8ZMp4qWpxWYDQmaA==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9076,7 +9218,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true + "dev": true, + "optional": true }, "uri-js": { "version": "4.2.2", @@ -9206,14 +9349,107 @@ } }, "watchpack": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz", - "integrity": "sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.2.tgz", + "integrity": "sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g==", "dev": true, "requires": { - "chokidar": "^2.0.2", + "chokidar": "^3.4.0", "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" + "neo-async": "^2.5.0", + "watchpack-chokidar2": "^2.0.0" + } + }, + "watchpack-chokidar2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz", + "integrity": "sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==", + "dev": true, + "optional": true, + "requires": { + "chokidar": "^2.1.8" + }, + "dependencies": { + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true, + "optional": true + }, + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", + "dev": true, + "optional": true, + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "optional": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "optional": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dev": true, + "optional": true, + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "optional": true + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dev": true, + "optional": true, + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + } } }, "web-ext-types": { @@ -9228,16 +9464,16 @@ "dev": true }, "webpack": { - "version": "4.41.5", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz", - "integrity": "sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", + "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.2.1", + "@webassemblyjs/ast": "1.9.0", + "@webassemblyjs/helper-module-context": "1.9.0", + "@webassemblyjs/wasm-edit": "1.9.0", + "@webassemblyjs/wasm-parser": "1.9.0", + "acorn": "^6.4.1", "ajv": "^6.10.2", "ajv-keywords": "^3.4.1", "chrome-trace-event": "^1.0.2", @@ -9248,22 +9484,37 @@ "loader-utils": "^1.2.3", "memory-fs": "^0.4.1", "micromatch": "^3.1.10", - "mkdirp": "^0.5.1", + "mkdirp": "^0.5.3", "neo-async": "^2.6.1", "node-libs-browser": "^2.2.1", "schema-utils": "^1.0.0", "tapable": "^1.1.3", "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.0", + "watchpack": "^1.6.1", "webpack-sources": "^1.4.1" }, "dependencies": { "acorn": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", - "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", "dev": true }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", @@ -9513,12 +9764,6 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - }, "worker-farm": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", @@ -9679,9 +9924,9 @@ } }, "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { "camelcase": "^5.0.0", diff --git a/package.json b/package.json index c78feeb..2ed46ac 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "tslint": "^5.20.1", "tslint-loader": "^3.6.0", "typescript": "^3.7.5", - "webpack": "^4.41.5", + "webpack": "^4.43.0", "webpack-cli": "^3.3.10", "webpack-merge": "^4.2.2", "zip-folder": "^1.0.0" @@ -29,8 +29,12 @@ "@types/jquery": "^3.3.31", "@types/loglevel": "^1.6.3", "@types/webappsec-credential-management": "^0.3.11", + "asn1js": "^2.0.26", + "bn.js": "^5.1.2", "cbor": "^4.3.0", - "jquery": "^3.4.1", + "elliptic": "^6.5.3", + "jquery": "^3.5.1", + "jsrsasign": "^8.0.20", "loglevel": "^1.6.6", "loglevel-plugin-prefix": "^0.8.4", "strip-sourcemap-loader": "^0.0.1", @@ -48,4 +52,4 @@ "postrelease": "node scripts/manifest.js", "release": "node scripts/release.js" } -} \ No newline at end of file +} diff --git a/src/crypto.ts b/src/crypto.ts index 2b2cef0..1f51c98 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,13 +1,14 @@ import * as CBOR from 'cbor'; import { getLogger } from './logging'; import { base64ToByteArray, byteArrayToBase64 } from './utils'; +import {Signature} from 'elliptic' const log = getLogger('crypto'); // Generated with pseudo random values via // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues export const CKEY_ID = new Uint8Array([ - 194547236, 76082241, 3628762690, 4137210381, + 194547238, 76082241, 3628762690, 4137210381, 1214244733, 1205845608, 840015201, 3897052717, 4072880437, 4027233456, 675224361, 2305433287, 74291263, 3461796691, 701523034, 3178201666, @@ -34,9 +35,9 @@ const coseEllipticCurveNames: { [s: number]: string } = { }; const ellipticNamedCurvesToCOSE: { [s: string]: number } = { - 'P-256': -7, - 'P-384': -35, - 'P-512': -36, + 'P-256': -7, // Just Support P-256 + //'P-384': -35, + //'P-512': -36, }; interface ICOSECompatibleKey { @@ -45,7 +46,7 @@ interface ICOSECompatibleKey { publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; generateAuthenticatorData(rpID: string, counter: number): Promise; - sign(clientData: string): Promise; + sign(clientData: Uint8Array): Promise; } class ECDSA implements ICOSECompatibleKey { @@ -139,8 +140,8 @@ class ECDSA implements ICOSECompatibleKey { if (this.publicKey) { // attestation flag goes on the 7th bit (from the right) authenticatorData[rpIdHash.length] |= (1 << 6); - offset++; } + offset++; // 4 bytes for the counter. big-endian uint32 // https://www.w3.org/TR/webauthn/#signature-counter @@ -169,15 +170,56 @@ class ECDSA implements ICOSECompatibleKey { return authenticatorData; } - public async sign(data: string): Promise { + // ToDo Fix signing + public async sign(data: Uint8Array): Promise { if (!this.privateKey) { throw new Error('no private key available for signing'); } - return window.crypto.subtle.sign( + const tmpsig = await window.crypto.subtle.sign( this.getKeyParams(), this.privateKey, - new TextEncoder().encode(data), - ); + data, + ) + + const rawSig = new Buffer(tmpsig) + + // Converting to DER encoding + /* const r = rawSig.slice(0, 32); + const s = rawSig.slice(32); + log.info("R and S"); + + var Signature = require("elliptic").signature; + log.info("Import"); + const sig = new Signature({r: new Uint8Array(r), s: new Uint8Array(s)}); + log.info("Signature"); + + const res = sig.toDER() + log.info("Converted"); + + return res;*/ + + const asn1 = require('asn1.js'); + const BN = require('bn.js'); + const crypto = require('crypto'); + + const EcdsaDerSig = asn1.define('ECPrivateKey', function() { + return this.seq().obj( + this.key('r').int(), + this.key('s').int() + ); + }); + + function asn1SigSigToConcatSig(asn1SigBuffer) { + const rsSig = EcdsaDerSig.decode(asn1SigBuffer, 'der'); + return Buffer.concat([ + rsSig.r.toArrayLike(Buffer, 'be', 32), + rsSig.s.toArrayLike(Buffer, 'be', 32) + ]); + } + + const r = new BN(rawSig.slice(0, 32).toString('hex'), 16, 'be'); + const s = new BN(rawSig.slice(32).toString('hex'), 16, 'be'); + return EcdsaDerSig.encode({r, s}, 'der'); } private getKeyParams(): EcdsaParams { diff --git a/src/storage.ts b/src/storage.ts index 4f0519e..7430370 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,5 +1,6 @@ import { ivLength, keyExportFormat, saltLength } from './constants'; import { base64ToByteArray, byteArrayToBase64, concatenate } from './utils'; +import {getLogger} from "./logging"; export const keyExists = (key: string): Promise => { return new Promise(async (res, rej) => { @@ -110,14 +111,24 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise ); }; +const log = getLogger('webauthn'); + export const fetchKey = async (key: string, pin: string): Promise => { + log.info("A") return new Promise(async (res, rej) => { + log.info("B") chrome.storage.sync.get(key, async (resp) => { + log.info("C") + log.info(key) if (!!chrome.runtime.lastError) { + log.info("D") rej(chrome.runtime.lastError); return; } + log.info("E") + log.info(resp.key) const payload = base64ToByteArray(resp[key]); + log.info("F") const saltByteLength = payload[0]; const ivByteLength = payload[1]; const keyAlgorithmByteLength = payload[2]; @@ -177,10 +188,14 @@ export const saveKey = (key: string, privateKey: CryptoKey, pin: string): Promis iv, keyAlgorithm, wrappedKey); + log.info(payload) + log.info(key) chrome.storage.sync.set({ [key]: byteArrayToBase64(payload) }, () => { if (!!chrome.runtime.lastError) { + log.info("Key not stored") rej(chrome.runtime.lastError); } else { + log.info("Key stored") res(); } }); diff --git a/src/utils.ts b/src/utils.ts index 43aedc8..ebc67a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,8 +69,9 @@ export function byteArrayToBase64(arr: Uint8Array, urlEncoded: boolean = false): const result = btoa(String.fromCharCode(...arr)); if (urlEncoded) { return result.replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } return result; } @@ -80,7 +81,8 @@ export function base64ToByteArray(str: string, urlEncoded: boolean = false): Uin if (urlEncoded) { rawInput = padString(rawInput) .replace(/\-/g, '+') - .replace(/_/g, '/'); + .replace(/_/g, '/') + .replace(/=/g, ""); } return Uint8Array.from(atob(rawInput), (c) => c.charCodeAt(0)); } diff --git a/src/webauthn.ts b/src/webauthn.ts index 9d0dd94..4e8a2f4 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,5 +1,5 @@ import * as CBOR from 'cbor'; -import { getCompatibleKey, getCompatibleKeyFromCryptoKey } from './crypto'; +import {CKEY_ID, getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; @@ -20,7 +20,9 @@ export const generateRegistrationKeyAndAttestation = async ( const rpID = rp.id || getDomainFromOrigin(origin); const user = publicKeyCreationOptions.user; const userID = byteArrayToBase64(new Uint8Array(user.id as ArrayBuffer)); - const keyID = window.btoa(`${userID}@${rpID}`); + log.info('userId', userID); + log.info('rpId', rpID); + const keyID = byteArrayToBase64(CKEY_ID, true); // First check if there is already a key for this rp ID if (await keyExists(keyID)) { @@ -30,20 +32,17 @@ export const generateRegistrationKeyAndAttestation = async ( const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter + // ToDo Use correct credential Id in authenticator & authenticator data const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, ); - const signature = await compatibleKey.sign(clientData); const attestationObject = CBOR.encodeCanonical({ - attStmt: { - alg: compatibleKey.algorithm, - sig: signature, - }, + attStmt: new Map(), authData: authenticatorData, - fmt: 'packed', + fmt: 'none', }).buffer; // Now that we have built all we need, let's save the key @@ -52,7 +51,7 @@ export const generateRegistrationKeyAndAttestation = async ( return { getClientExtensionResults: () => ({}), id: keyID, - rawId: base64ToByteArray(keyID), + rawId: base64ToByteArray(keyID, true), response: { attestationObject, clientDataJSON: base64ToByteArray(window.btoa(clientData)), @@ -61,6 +60,7 @@ export const generateRegistrationKeyAndAttestation = async ( } as PublicKeyCredential; }; +// Assertion export const generateKeyRequestAndAttestation = async ( origin: string, publicKeyRequestOptions: PublicKeyCredentialRequestOptions, @@ -70,11 +70,17 @@ export const generateKeyRequestAndAttestation = async ( log.debug('No keys requested'); return null; } + log.info('origin', origin); + origin = 'http://localhost:9005'; // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; const keyIDArray: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const keyID = byteArrayToBase64(new Uint8Array(keyIDArray)); + const keyID = byteArrayToBase64(new Uint8Array(keyIDArray), true); + log.info("Hier") + log.info(`keyID`, keyID) const key = await fetchKey(keyID, pin); + log.info('key', key) + log.info("nicht") if (!key) { throw new Error(`key with id ${keyID} not found`); @@ -87,19 +93,29 @@ export const generateKeyRequestAndAttestation = async ( tokenBinding: { status: 'not-supported', }, - type: 'webauthn.create', + type: 'webauthn.get', }, ); - const signature = await compatibleKey.sign(clientData); + const clientDataJSON = base64ToByteArray(window.btoa(clientData)); + const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); + + const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); + concatData.set(authenticatorData); + concatData.set(clientDataHash, authenticatorData.length); + log.info('concatData', concatData); + + const signature = await compatibleKey.sign(concatData); + log.info('signature', signature); return { id: keyID, rawId: keyIDArray, response: { authenticatorData: authenticatorData.buffer, - clientDataJSON: base64ToByteArray(window.btoa(clientData)), - signature, + clientDataJSON: clientDataJSON, + signature: (new Uint8Array(signature)).buffer, userHandle: new ArrayBuffer(0), // This should be nullable }, type: 'public-key', From d08fd5f44ae84222b85903b9c4e03b79425e7971 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 1 Jul 2020 17:27:18 +0200 Subject: [PATCH 02/81] Clean up --- src/background.ts | 4 +-- src/crypto.ts | 75 +++++++++++++---------------------------------- src/storage.ts | 5 ---- src/webauthn.ts | 49 +++++++++++++++---------------- 4 files changed, 46 insertions(+), 87 deletions(-) diff --git a/src/background.ts b/src/background.ts index b378a08..27ab8c2 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,7 +1,7 @@ import { disabledIcons, enabledIcons } from './constants'; import { getLogger } from './logging'; import { getOriginFromUrl, webauthnParse, webauthnStringify } from './utils'; -import { generateKeyRequestAndAttestation, generateRegistrationKeyAndAttestation } from './webauthn'; +import { generateKeyRequestAndAssertion, generateRegistrationKeyAndAttestation } from './webauthn'; const log = getLogger('background'); @@ -69,7 +69,7 @@ const sign = async (msg, sender: chrome.runtime.MessageSender) => { const pin = await requestPin(sender.tab.id, origin); try { - const credential = await generateKeyRequestAndAttestation(origin, opts.publicKey, `${pin}`); + const credential = await generateKeyRequestAndAssertion(origin, opts.publicKey, `${pin}`); const authenticatedResponseData = { credential: webauthnStringify(credential), requestID: msg.requestID, diff --git a/src/crypto.ts b/src/crypto.ts index 1f51c98..319bed4 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,19 +1,18 @@ import * as CBOR from 'cbor'; import { getLogger } from './logging'; import { base64ToByteArray, byteArrayToBase64 } from './utils'; -import {Signature} from 'elliptic' const log = getLogger('crypto'); -// Generated with pseudo random values via -// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues -export const CKEY_ID = new Uint8Array([ - 194547238, 76082241, 3628762690, 4137210381, - 1214244733, 1205845608, 840015201, 3897052717, - 4072880437, 4027233456, 675224361, 2305433287, - 74291263, 3461796691, 701523034, 3178201666, - 3992003567, 1410532, 4234129691, 1438515639, -]); +export function createCredentialId(): Uint8Array{ + let dt = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (dt + Math.random()*16)%16 | 0; + dt = Math.floor(dt/16); + return (c=='x' ? r :(r&0x3|0x8)).toString(16); + }); + return base64ToByteArray(uuid, true); +} // Copied from krypton function counterToBytes(c: number): Uint8Array { @@ -30,14 +29,10 @@ function counterToBytes(c: number): Uint8Array { const coseEllipticCurveNames: { [s: number]: string } = { 1: 'SHA-256', - 2: 'SHA-384', - 3: 'SHA-512', }; const ellipticNamedCurvesToCOSE: { [s: string]: number } = { - 'P-256': -7, // Just Support P-256 - //'P-384': -35, - //'P-512': -36, + 'P-256': -7, }; interface ICOSECompatibleKey { @@ -45,7 +40,7 @@ interface ICOSECompatibleKey { privateKey: CryptoKey; publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; - generateAuthenticatorData(rpID: string, counter: number): Promise; + generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array): Promise; sign(clientData: Uint8Array): Promise; } @@ -81,8 +76,6 @@ class ECDSA implements ICOSECompatibleKey { */ private static ellipticCurveKeys: { [s: number]: number } = { [-7]: 1, - [-35]: 2, - [-36]: 3, }; constructor( @@ -103,7 +96,7 @@ class ECDSA implements ICOSECompatibleKey { }); } - public async generateAuthenticatorData(rpID: string, counter: number): Promise { + public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); @@ -114,16 +107,16 @@ class ECDSA implements ICOSECompatibleKey { let authenticatorDataLength = rpIdHash.length + 1 + 4; if (this.publicKey) { - aaguid = CKEY_ID.slice(0, 16); + aaguid = credentialId.slice(0, 16); // 16-bit unsigned big-endian integer. credIdLen = new Uint8Array(2); - credIdLen[0] = (CKEY_ID.length >> 8) & 0xff; - credIdLen[1] = CKEY_ID.length & 0xff; + credIdLen[0] = (credentialId.length >> 8) & 0xff; + credIdLen[1] = credentialId.length & 0xff; const coseKey = await this.toCOSE(this.publicKey); encodedKey = new Uint8Array(CBOR.encode(coseKey)); authenticatorDataLength += aaguid.length + credIdLen.byteLength - + CKEY_ID.length + + credentialId.length + encodedKey.byteLength; } @@ -161,8 +154,8 @@ class ECDSA implements ICOSECompatibleKey { offset += credIdLen.byteLength; // Variable length authenticator key ID - authenticatorData.set(CKEY_ID, offset); - offset += CKEY_ID.length; + authenticatorData.set(credentialId, offset); + offset += credentialId.length; // Variable length public key authenticatorData.set(encodedKey, offset); @@ -170,37 +163,21 @@ class ECDSA implements ICOSECompatibleKey { return authenticatorData; } - // ToDo Fix signing public async sign(data: Uint8Array): Promise { if (!this.privateKey) { throw new Error('no private key available for signing'); } - const tmpsig = await window.crypto.subtle.sign( + const tmpSign = await window.crypto.subtle.sign( this.getKeyParams(), this.privateKey, data, ) - const rawSig = new Buffer(tmpsig) - - // Converting to DER encoding - /* const r = rawSig.slice(0, 32); - const s = rawSig.slice(32); - log.info("R and S"); - - var Signature = require("elliptic").signature; - log.info("Import"); - const sig = new Signature({r: new Uint8Array(r), s: new Uint8Array(s)}); - log.info("Signature"); - - const res = sig.toDER() - log.info("Converted"); - - return res;*/ + const rawSig = new Buffer(tmpSign) + // Credit to: https://stackoverflow.com/a/39651457/5333936 const asn1 = require('asn1.js'); const BN = require('bn.js'); - const crypto = require('crypto'); const EcdsaDerSig = asn1.define('ECPrivateKey', function() { return this.seq().obj( @@ -209,14 +186,6 @@ class ECDSA implements ICOSECompatibleKey { ); }); - function asn1SigSigToConcatSig(asn1SigBuffer) { - const rsSig = EcdsaDerSig.decode(asn1SigBuffer, 'der'); - return Buffer.concat([ - rsSig.r.toArrayLike(Buffer, 'be', 32), - rsSig.s.toArrayLike(Buffer, 'be', 32) - ]); - } - const r = new BN(rawSig.slice(0, 32).toString('hex'), 16, 'be'); const s = new BN(rawSig.slice(32).toString('hex'), 16, 'be'); return EcdsaDerSig.encode({r, s}, 'der'); @@ -245,8 +214,6 @@ class ECDSA implements ICOSECompatibleKey { const defaultPKParams = { alg: -7, type: 'public-key' }; const coseAlgorithmToKeyName = { [-7]: 'ECDSA', - [-35]: 'ECDSA', - [-36]: 'ECDSA', }; export const getCompatibleKey = (pkParams: PublicKeyCredentialParameters[]): Promise => { diff --git a/src/storage.ts b/src/storage.ts index 7430370..9617359 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -116,19 +116,14 @@ const log = getLogger('webauthn'); export const fetchKey = async (key: string, pin: string): Promise => { log.info("A") return new Promise(async (res, rej) => { - log.info("B") chrome.storage.sync.get(key, async (resp) => { - log.info("C") log.info(key) if (!!chrome.runtime.lastError) { - log.info("D") rej(chrome.runtime.lastError); return; } - log.info("E") log.info(resp.key) const payload = base64ToByteArray(resp[key]); - log.info("F") const saltByteLength = payload[0]; const ivByteLength = payload[1]; const keyAlgorithmByteLength = payload[2]; diff --git a/src/webauthn.ts b/src/webauthn.ts index 4e8a2f4..ee73722 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,5 +1,5 @@ import * as CBOR from 'cbor'; -import {CKEY_ID, getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; +import {createCredentialId, getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; @@ -20,20 +20,20 @@ export const generateRegistrationKeyAndAttestation = async ( const rpID = rp.id || getDomainFromOrigin(origin); const user = publicKeyCreationOptions.user; const userID = byteArrayToBase64(new Uint8Array(user.id as ArrayBuffer)); - log.info('userId', userID); - log.info('rpId', rpID); - const keyID = byteArrayToBase64(CKEY_ID, true); + + const credentialId = createCredentialId(); + const encCredId = byteArrayToBase64(credentialId, true); // First check if there is already a key for this rp ID - if (await keyExists(keyID)) { - throw new Error(`key with id ${keyID} already exists`); + if (await keyExists(encCredId)) { + throw new Error(`key with id ${encCredId} already exists`); } - log.debug('key ID', keyID); + const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter // ToDo Use correct credential Id in authenticator & authenticator data - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId); const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, @@ -46,12 +46,12 @@ export const generateRegistrationKeyAndAttestation = async ( }).buffer; // Now that we have built all we need, let's save the key - await saveKey(keyID, compatibleKey.privateKey, pin); + await saveKey(encCredId, compatibleKey.privateKey, pin); return { getClientExtensionResults: () => ({}), - id: keyID, - rawId: base64ToByteArray(keyID, true), + id: encCredId, + rawId: credentialId, response: { attestationObject, clientDataJSON: base64ToByteArray(window.btoa(clientData)), @@ -61,7 +61,7 @@ export const generateRegistrationKeyAndAttestation = async ( }; // Assertion -export const generateKeyRequestAndAttestation = async ( +export const generateKeyRequestAndAssertion = async ( origin: string, publicKeyRequestOptions: PublicKeyCredentialRequestOptions, pin: string, @@ -70,20 +70,18 @@ export const generateKeyRequestAndAttestation = async ( log.debug('No keys requested'); return null; } - log.info('origin', origin); - origin = 'http://localhost:9005'; + + origin = 'http://localhost:9005'; // Given origin does not work! + // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; - const keyIDArray: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const keyID = byteArrayToBase64(new Uint8Array(keyIDArray), true); - log.info("Hier") - log.info(`keyID`, keyID) - const key = await fetchKey(keyID, pin); - log.info('key', key) - log.info("nicht") + const credentialId: ArrayBuffer = requestedCredential.id as ArrayBuffer; + const endCredId = byteArrayToBase64(new Uint8Array(credentialId), true); + + const key = await fetchKey(endCredId, pin); if (!key) { - throw new Error(`key with id ${keyID} not found`); + throw new Error(`key with id ${endCredId} not found`); } const compatibleKey = await getCompatibleKeyFromCryptoKey(key); const clientData = await compatibleKey.generateClientData( @@ -100,18 +98,17 @@ export const generateKeyRequestAndAttestation = async ( const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array()); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); concatData.set(clientDataHash, authenticatorData.length); - log.info('concatData', concatData); const signature = await compatibleKey.sign(concatData); log.info('signature', signature); return { - id: keyID, - rawId: keyIDArray, + id: endCredId, + rawId: credentialId, response: { authenticatorData: authenticatorData.buffer, clientDataJSON: clientDataJSON, From b1a94bab7864e46e38451e59b1ca0a0739b6dcc0 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 2 Jul 2020 18:04:40 +0200 Subject: [PATCH 03/81] Add recovery key loading --- dist/chromium/manifest.json | 3 +- src/crypto.ts | 2 +- src/recovery.ts | 94 +++++++++++++++++++++++++++++++++++++ src/storage.ts | 4 +- src/webauthn.ts | 12 +++-- 5 files changed, 106 insertions(+), 9 deletions(-) create mode 100644 src/recovery.ts diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index 0f8b3cf..07ec167 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -50,6 +50,7 @@ ], "web_accessible_resources": [ "js/inject_webauthn.js", - "img/*" + "img/*", + "recovery/*" ] } \ No newline at end of file diff --git a/src/crypto.ts b/src/crypto.ts index 319bed4..be267f2 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -37,7 +37,7 @@ const ellipticNamedCurvesToCOSE: { [s: string]: number } = { interface ICOSECompatibleKey { algorithm: number; - privateKey: CryptoKey; + privateKey?: CryptoKey; publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array): Promise; diff --git a/src/recovery.ts b/src/recovery.ts new file mode 100644 index 0000000..d031d1d --- /dev/null +++ b/src/recovery.ts @@ -0,0 +1,94 @@ +import {getLogger} from "./logging"; +import {base64ToByteArray} from "./utils"; +import {keyExportFormat} from "./constants"; + +const log = getLogger('recovery'); + +export async function syncBackupKeys () { + const bckpKeys = await loadBackupKeys(); + log.info("Loaded backup keys", bckpKeys); + await storeBackupKeys("backup", bckpKeys) +} + +export class BackupKey { + key: CryptoKey; + id: string; + constructor(key: CryptoKey, id: string) { + this.key = key; + this.id = id; + } +} + +export async function loadBackupKeys(): Promise> { + log.info("Loading backup keys form JSON file") + return new Promise>(function (resolve, reject) { + let xhr = new XMLHttpRequest(); + xhr.open("GET", chrome.extension.getURL('/recovery/backup.json'), true); + xhr.onload = function () { + let status = xhr.status; + if (status == 200) { + let jwk = JSON.parse(this.response); + let i; + let bckpKeys = new Array() + for (i = 0; i < jwk.length; ++i) { + bckpKeys.push(new BackupKey(jwk[i], jwk[i].kid)); + } + resolve(bckpKeys); + } else { + reject(status); + } + }; + xhr.send(); + }); +} + +async function parseKey(jwk): Promise { + return window.crypto.subtle.importKey( + "jwk", + jwk, + { + name: "ECDSA", + namedCurve: "P-256" + }, + true, + [] + ); +} + +async function storeBackupKeys(identifier: string, backupKeys: Array): Promise { + log.info("Storing backup keys") + let bckpJSON = await JSON.stringify(backupKeys); + return new Promise(async (res, rej) => { + chrome.storage.sync.set({ [identifier]: bckpJSON }, () => { + if (!!chrome.runtime.lastError) { + log.info("Backup keys not stored") + rej(chrome.runtime.lastError); + } else { + log.info("Backup keys stored") + res(); + } + }); + }); +} + +async function fetchBackupKeys(identifier: string): Promise> { + return new Promise>(async (res, rej) => { + chrome.storage.sync.get(identifier, async (resp) => { + if (!!chrome.runtime.lastError) { + log.info("Could not fetch backup keys"); + rej(chrome.runtime.lastError); + return; + } + let bckpKeys = await JSON.parse(resp[identifier]); + log.info(bckpKeys); + res(bckpKeys); + }); + }); +} + +export async function popBackupKey(identifier: string = "backup"): Promise { + let bckpKeys = await fetchBackupKeys(identifier); + let key = bckpKeys.pop(); + await storeBackupKeys(identifier, bckpKeys) + return key; +} \ No newline at end of file diff --git a/src/storage.ts b/src/storage.ts index 9617359..bb9e50d 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -114,7 +114,6 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise const log = getLogger('webauthn'); export const fetchKey = async (key: string, pin: string): Promise => { - log.info("A") return new Promise(async (res, rej) => { chrome.storage.sync.get(key, async (resp) => { log.info(key) @@ -183,8 +182,7 @@ export const saveKey = (key: string, privateKey: CryptoKey, pin: string): Promis iv, keyAlgorithm, wrappedKey); - log.info(payload) - log.info(key) + chrome.storage.sync.set({ [key]: byteArrayToBase64(payload) }, () => { if (!!chrome.runtime.lastError) { log.info("Key not stored") diff --git a/src/webauthn.ts b/src/webauthn.ts index ee73722..db73768 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -3,6 +3,7 @@ import {createCredentialId, getCompatibleKey, getCompatibleKeyFromCryptoKey} fro import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; +import {loadBackupKeys, popBackupKey, syncBackupKeys} from "./recovery"; const log = getLogger('webauthn'); @@ -11,9 +12,8 @@ export const generateRegistrationKeyAndAttestation = async ( publicKeyCreationOptions: PublicKeyCredentialCreationOptions, pin: string, ): Promise => { - if (publicKeyCreationOptions.attestation === 'direct') { - log.warn('We are being requested to create a key with "direct" attestation'); - log.warn(`We can only perform self-attestation, therefore we will not be provisioning any keys`); + if (publicKeyCreationOptions.attestation !== 'none') { + log.warn('We can perform only none attestation'); return null; } const rp = publicKeyCreationOptions.rp; @@ -29,10 +29,14 @@ export const generateRegistrationKeyAndAttestation = async ( throw new Error(`key with id ${encCredId} already exists`); } + await syncBackupKeys(); + let key = await popBackupKey(); + log.info(key); + return; + const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter - // ToDo Use correct credential Id in authenticator & authenticator data const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId); const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, From bed610fcd65cebfce4ce4348c78b0d0f7609d1e0 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 3 Jul 2020 10:22:03 +0200 Subject: [PATCH 04/81] Export/Import crypto key for storage --- src/recovery.ts | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index d031d1d..11c6018 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -24,16 +24,17 @@ export async function loadBackupKeys(): Promise> { return new Promise>(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open("GET", chrome.extension.getURL('/recovery/backup.json'), true); - xhr.onload = function () { + xhr.onload = async function () { let status = xhr.status; if (status == 200) { let jwk = JSON.parse(this.response); let i; let bckpKeys = new Array() for (i = 0; i < jwk.length; ++i) { - bckpKeys.push(new BackupKey(jwk[i], jwk[i].kid)); + let parsedKey = await parseKey(jwk[i]); + bckpKeys.push(new BackupKey(parsedKey, jwk[i].kid)); } - resolve(bckpKeys); + await resolve(bckpKeys); } else { reject(status); } @@ -55,9 +56,28 @@ async function parseKey(jwk): Promise { ); } +class ExportKey { + key: JsonWebKey; + id: string; + constructor(key: JsonWebKey, id: string) { + this.key = key; + this.id = id; + } +} + + + async function storeBackupKeys(identifier: string, backupKeys: Array): Promise { - log.info("Storing backup keys") - let bckpJSON = await JSON.stringify(backupKeys); + let exportKeys = new Array(); + let i; + for (i = 0; i < backupKeys.length; ++i) { + let parsedKey = await window.crypto.subtle.exportKey("jwk", backupKeys[i].key); + exportKeys.push(new ExportKey(parsedKey, backupKeys[i].id)); + } + let bckpJSON = JSON.stringify(exportKeys); + log.info("Storing backup keys", bckpJSON); + + // ToDo Export key on storage and stringify, import on load return new Promise(async (res, rej) => { chrome.storage.sync.set({ [identifier]: bckpJSON }, () => { if (!!chrome.runtime.lastError) { @@ -79,7 +99,14 @@ async function fetchBackupKeys(identifier: string): Promise> { rej(chrome.runtime.lastError); return; } - let bckpKeys = await JSON.parse(resp[identifier]); + + let exportedKey = await JSON.parse(resp[identifier]); + let bckpKeys = new Array(); + let i; + for (i = 0; i < exportedKey.length; ++i) { + let parsedKey = await parseKey(exportedKey[i].key); + bckpKeys.push(new BackupKey(parsedKey, exportedKey[i].id)); + } log.info(bckpKeys); res(bckpKeys); }); From b6cbcec4221dcda78791bd619c4a9ae66727acdd Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 3 Jul 2020 10:43:51 +0200 Subject: [PATCH 05/81] Use credential id of backup key --- src/webauthn.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/webauthn.ts b/src/webauthn.ts index db73768..ba1adb9 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -18,10 +18,11 @@ export const generateRegistrationKeyAndAttestation = async ( } const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - const user = publicKeyCreationOptions.user; - const userID = byteArrayToBase64(new Uint8Array(user.id as ArrayBuffer)); - const credentialId = createCredentialId(); + let bckpKey = await popBackupKey(); + log.info('Used backup key', bckpKey); + + const credentialId = base64ToByteArray(bckpKey.id, true);; const encCredId = byteArrayToBase64(credentialId, true); // First check if there is already a key for this rp ID @@ -29,12 +30,9 @@ export const generateRegistrationKeyAndAttestation = async ( throw new Error(`key with id ${encCredId} already exists`); } - await syncBackupKeys(); - let key = await popBackupKey(); - log.info(key); - return; + // await syncBackupKeys(); - const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); + let compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId); From 5d5dacfca6d15285904b01cade740d8cbcc3b4c2 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 3 Jul 2020 17:18:57 +0200 Subject: [PATCH 06/81] Add psk registration flow --- src/crypto.ts | 28 ++++++++++++++++++++++------ src/recovery.ts | 14 ++++++++++++-- src/webauthn.ts | 19 +++++++++++++------ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index be267f2..82938c5 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -40,8 +40,9 @@ interface ICOSECompatibleKey { privateKey?: CryptoKey; publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; - generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array): Promise; + generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, extensionOutput: Uint8Array): Promise; sign(clientData: Uint8Array): Promise; + toCOSE(key: CryptoKey): Promise>; } class ECDSA implements ICOSECompatibleKey { @@ -96,7 +97,7 @@ class ECDSA implements ICOSECompatibleKey { }); } - public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array): Promise { + public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, extensionOutput: Uint8Array = null): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); @@ -120,6 +121,10 @@ class ECDSA implements ICOSECompatibleKey { + encodedKey.byteLength; } + if (extensionOutput != null) { + authenticatorDataLength += extensionOutput.byteLength; + } + const authenticatorData = new Uint8Array(authenticatorDataLength); let offset = 0; @@ -128,11 +133,13 @@ class ECDSA implements ICOSECompatibleKey { offset += rpIdHash.length; // 1 byte for flags - // user-presence flag goes on the right-most bit - authenticatorData[rpIdHash.length] = 1; + authenticatorData[rpIdHash.length] = 1; // User presence (Bit 0) if (this.publicKey) { // attestation flag goes on the 7th bit (from the right) - authenticatorData[rpIdHash.length] |= (1 << 6); + authenticatorData[rpIdHash.length] |= (1 << 6); // Attestation present (Bit 6) + } + if (extensionOutput != null) { + authenticatorData[rpIdHash.length] |= (1 << 7); // Extension present (Bit 7) } offset++; @@ -145,6 +152,8 @@ class ECDSA implements ICOSECompatibleKey { return authenticatorData; } + // attestedCredentialData + // 16 bytes for the Authenticator Attestation GUID authenticatorData.set(aaguid, offset); offset += aaguid.length; @@ -159,6 +168,13 @@ class ECDSA implements ICOSECompatibleKey { // Variable length public key authenticatorData.set(encodedKey, offset); + offset += encodedKey.byteLength; + + // Variable length for extension + // ToDo Handle extension for assertion + if (extensionOutput != null) { + authenticatorData.set(extensionOutput, offset); + } return authenticatorData; } @@ -195,7 +211,7 @@ class ECDSA implements ICOSECompatibleKey { return { name: 'ECDSA', hash: coseEllipticCurveNames[ECDSA.ellipticCurveKeys[this.algorithm]] }; } - private async toCOSE(key: CryptoKey): Promise> { + public async toCOSE(key: CryptoKey): Promise> { // In JWK the X and Y portions are Base64URL encoded (https://tools.ietf.org/html/rfc7517#section-3), // which is just the right type for COSE encoding (https://tools.ietf.org/html/rfc8152#section-7), // we just need to convert it to a byte array. diff --git a/src/recovery.ts b/src/recovery.ts index 11c6018..6f1cdd2 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,9 +1,11 @@ +import * as CBOR from 'cbor'; import {getLogger} from "./logging"; -import {base64ToByteArray} from "./utils"; -import {keyExportFormat} from "./constants"; +import {getCompatibleKeyFromCryptoKey} from "./crypto"; const log = getLogger('recovery'); +export const PSK: string = "psk" + export async function syncBackupKeys () { const bckpKeys = await loadBackupKeys(); log.info("Loaded backup keys", bckpKeys); @@ -118,4 +120,12 @@ export async function popBackupKey(identifier: string = "backup"): Promise { + let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); + let coseKey = await new Uint8Array(CBOR.encode(compatibleKey.toCOSE(backupKey.key))); + + let extOutput = new Map([[PSK, coseKey]]); + return new Uint8Array(CBOR.encode(extOutput)); } \ No newline at end of file diff --git a/src/webauthn.ts b/src/webauthn.ts index ba1adb9..7ad26e7 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -3,7 +3,7 @@ import {createCredentialId, getCompatibleKey, getCompatibleKeyFromCryptoKey} fro import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import {loadBackupKeys, popBackupKey, syncBackupKeys} from "./recovery"; +import {loadBackupKeys, popBackupKey, pskOutput, syncBackupKeys} from "./recovery"; const log = getLogger('webauthn'); @@ -16,13 +16,20 @@ export const generateRegistrationKeyAndAttestation = async ( log.warn('We can perform only none attestation'); return null; } + log.info(JSON.stringify(publicKeyCreationOptions.extensions)); + // ToDo Trigger PSK flow only if RP signals extension support + const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); + // await syncBackupKeys(); + let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); - const credentialId = base64ToByteArray(bckpKey.id, true);; + const pskExt = await pskOutput(bckpKey); + + const credentialId = base64ToByteArray(bckpKey.id, true); const encCredId = byteArrayToBase64(credentialId, true); // First check if there is already a key for this rp ID @@ -30,12 +37,10 @@ export const generateRegistrationKeyAndAttestation = async ( throw new Error(`key with id ${encCredId} already exists`); } - // await syncBackupKeys(); - let compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId, pskExt); const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, @@ -50,6 +55,8 @@ export const generateRegistrationKeyAndAttestation = async ( // Now that we have built all we need, let's save the key await saveKey(encCredId, compatibleKey.privateKey, pin); + log.debug('send attestation'); + return { getClientExtensionResults: () => ({}), id: encCredId, @@ -100,7 +107,7 @@ export const generateKeyRequestAndAssertion = async ( const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array()); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); From 87298a0659d76d9e34062210c67f09e0bcd85354 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 5 Jul 2020 18:38:06 +0200 Subject: [PATCH 07/81] Typo --- src/recovery.ts | 1 - src/webauthn.ts | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 6f1cdd2..3fb8c67 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -79,7 +79,6 @@ async function storeBackupKeys(identifier: string, backupKeys: Array) let bckpJSON = JSON.stringify(exportKeys); log.info("Storing backup keys", bckpJSON); - // ToDo Export key on storage and stringify, import on load return new Promise(async (res, rej) => { chrome.storage.sync.set({ [identifier]: bckpJSON }, () => { if (!!chrome.runtime.lastError) { diff --git a/src/webauthn.ts b/src/webauthn.ts index 7ad26e7..05b5b7a 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -85,12 +85,12 @@ export const generateKeyRequestAndAssertion = async ( // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; const credentialId: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const endCredId = byteArrayToBase64(new Uint8Array(credentialId), true); + const encCredId = byteArrayToBase64(new Uint8Array(credentialId), true); - const key = await fetchKey(endCredId, pin); + const key = await fetchKey(encCredId, pin); if (!key) { - throw new Error(`key with id ${endCredId} not found`); + throw new Error(`key with id ${encCredId} not found`); } const compatibleKey = await getCompatibleKeyFromCryptoKey(key); const clientData = await compatibleKey.generateClientData( @@ -116,7 +116,7 @@ export const generateKeyRequestAndAssertion = async ( const signature = await compatibleKey.sign(concatData); log.info('signature', signature); return { - id: endCredId, + id: encCredId, rawId: credentialId, response: { authenticatorData: authenticatorData.buffer, From 87bc6178477594c8d9a75a8b5cc73a5d9084c7d3 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 6 Jul 2020 18:56:25 +0200 Subject: [PATCH 08/81] Adapt method names --- src/crypto.ts | 2 +- src/recovery.ts | 82 ++++++++++++++++++++++++++++--------------------- src/webauthn.ts | 8 ++--- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 82938c5..93a6955 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -11,7 +11,7 @@ export function createCredentialId(): Uint8Array{ dt = Math.floor(dt/16); return (c=='x' ? r :(r&0x3|0x8)).toString(16); }); - return base64ToByteArray(uuid, true); + return base64ToByteArray(window.btoa(uuid), true); } // Copied from krypton diff --git a/src/recovery.ts b/src/recovery.ts index 3fb8c67..db2035a 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -5,14 +5,16 @@ import {getCompatibleKeyFromCryptoKey} from "./crypto"; const log = getLogger('recovery'); export const PSK: string = "psk" +const BACKUP: string = "backup" +const RECOVERY: string = "recovery" export async function syncBackupKeys () { const bckpKeys = await loadBackupKeys(); log.info("Loaded backup keys", bckpKeys); - await storeBackupKeys("backup", bckpKeys) + await storePSKKeys(BACKUP, bckpKeys) } -export class BackupKey { +class PSKKey { key: CryptoKey; id: string; constructor(key: CryptoKey, id: string) { @@ -21,6 +23,21 @@ export class BackupKey { } } +class ExportKey { + key: JsonWebKey; + id: string; + constructor(key: JsonWebKey, id: string) { + this.key = key; + this.id = id; + } +} + +export class BackupKey extends PSKKey { +} + +export class RecoveryKey extends PSKKey { +} + export async function loadBackupKeys(): Promise> { log.info("Loading backup keys form JSON file") return new Promise>(function (resolve, reject) { @@ -33,7 +50,7 @@ export async function loadBackupKeys(): Promise> { let i; let bckpKeys = new Array() for (i = 0; i < jwk.length; ++i) { - let parsedKey = await parseKey(jwk[i]); + let parsedKey = await parseJWK(jwk[i]); bckpKeys.push(new BackupKey(parsedKey, jwk[i].kid)); } await resolve(bckpKeys); @@ -45,7 +62,7 @@ export async function loadBackupKeys(): Promise> { }); } -async function parseKey(jwk): Promise { +async function parseJWK(jwk): Promise { return window.crypto.subtle.importKey( "jwk", jwk, @@ -58,70 +75,65 @@ async function parseKey(jwk): Promise { ); } -class ExportKey { - key: JsonWebKey; - id: string; - constructor(key: JsonWebKey, id: string) { - this.key = key; - this.id = id; - } -} - -async function storeBackupKeys(identifier: string, backupKeys: Array): Promise { +async function storePSKKeys(identifier: string, psk: Array): Promise { let exportKeys = new Array(); let i; - for (i = 0; i < backupKeys.length; ++i) { - let parsedKey = await window.crypto.subtle.exportKey("jwk", backupKeys[i].key); - exportKeys.push(new ExportKey(parsedKey, backupKeys[i].id)); + for (i = 0; i < psk.length; ++i) { + let parsedKey = await window.crypto.subtle.exportKey("jwk", psk[i].key); + exportKeys.push(new ExportKey(parsedKey, psk[i].id)); } - let bckpJSON = JSON.stringify(exportKeys); - log.info("Storing backup keys", bckpJSON); + let pskJSON = JSON.stringify(exportKeys); + log.info("Storing psk keys", pskJSON); return new Promise(async (res, rej) => { - chrome.storage.sync.set({ [identifier]: bckpJSON }, () => { + chrome.storage.sync.set({ [identifier]: pskJSON }, () => { if (!!chrome.runtime.lastError) { - log.info("Backup keys not stored") + log.info("PSK keys not stored") rej(chrome.runtime.lastError); } else { - log.info("Backup keys stored") + log.info("PSK keys stored") res(); } }); }); } -async function fetchBackupKeys(identifier: string): Promise> { - return new Promise>(async (res, rej) => { +async function fetchPSKKeys(identifier: string): Promise> { + return new Promise>(async (res, rej) => { chrome.storage.sync.get(identifier, async (resp) => { if (!!chrome.runtime.lastError) { - log.info("Could not fetch backup keys"); + log.info("Could not fetch PSK keys"); rej(chrome.runtime.lastError); return; } let exportedKey = await JSON.parse(resp[identifier]); - let bckpKeys = new Array(); + let pskKeys = new Array(); let i; for (i = 0; i < exportedKey.length; ++i) { - let parsedKey = await parseKey(exportedKey[i].key); - bckpKeys.push(new BackupKey(parsedKey, exportedKey[i].id)); + let parsedKey = await parseJWK(exportedKey[i].key); + pskKeys.push(new PSKKey(parsedKey, exportedKey[i].id)); } - log.info(bckpKeys); - res(bckpKeys); + log.info(pskKeys); + res(pskKeys); }); }); } -export async function popBackupKey(identifier: string = "backup"): Promise { - let bckpKeys = await fetchBackupKeys(identifier); - let key = bckpKeys.pop(); - await storeBackupKeys(identifier, bckpKeys) +async function popPSKKey(identifier: string): Promise { + let pskKeys = await fetchPSKKeys(identifier); + let key = pskKeys.pop(); + await storePSKKeys(identifier, pskKeys) return key; } -export async function pskOutput(backupKey: BackupKey): Promise { +export async function popBackupKey(): Promise { + return popPSKKey(BACKUP); +} + +export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); let coseKey = await new Uint8Array(CBOR.encode(compatibleKey.toCOSE(backupKey.key))); diff --git a/src/webauthn.ts b/src/webauthn.ts index 05b5b7a..dc603ca 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -3,7 +3,7 @@ import {createCredentialId, getCompatibleKey, getCompatibleKeyFromCryptoKey} fro import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import {loadBackupKeys, popBackupKey, pskOutput, syncBackupKeys} from "./recovery"; +import {popBackupKey, pskSetupExtensionOutput, syncBackupKeys} from "./recovery"; const log = getLogger('webauthn'); @@ -22,17 +22,17 @@ export const generateRegistrationKeyAndAttestation = async ( const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - // await syncBackupKeys(); + // await syncBackupKeys(); // ToDo Add own method to trigger sync let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); - const pskExt = await pskOutput(bckpKey); + const pskExt = await pskSetupExtensionOutput(bckpKey); const credentialId = base64ToByteArray(bckpKey.id, true); const encCredId = byteArrayToBase64(credentialId, true); - // First check if there is already a key for this rp ID + // Check if there is already a key for this rp ID if (await keyExists(encCredId)) { throw new Error(`key with id ${encCredId} already exists`); } From 5349bd7d220b3e44a6eedd3973a9076c0824894d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 6 Jul 2020 20:15:29 +0200 Subject: [PATCH 09/81] Recovery key download --- src/crypto.ts | 10 ---------- src/recovery.ts | 47 +++++++++++++++++++++++++++++++++++++++++++++++ src/webauthn.ts | 5 +++-- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 93a6955..30def99 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -4,16 +4,6 @@ import { base64ToByteArray, byteArrayToBase64 } from './utils'; const log = getLogger('crypto'); -export function createCredentialId(): Uint8Array{ - let dt = new Date().getTime(); - const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = (dt + Math.random()*16)%16 | 0; - dt = Math.floor(dt/16); - return (c=='x' ? r :(r&0x3|0x8)).toString(16); - }); - return base64ToByteArray(window.btoa(uuid), true); -} - // Copied from krypton function counterToBytes(c: number): Uint8Array { const bytes = new Uint8Array(4); diff --git a/src/recovery.ts b/src/recovery.ts index db2035a..93df911 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,6 +1,7 @@ import * as CBOR from 'cbor'; import {getLogger} from "./logging"; import {getCompatibleKeyFromCryptoKey} from "./crypto"; +import {base64ToByteArray, byteArrayToBase64} from "./utils"; const log = getLogger('recovery'); @@ -36,6 +37,9 @@ export class BackupKey extends PSKKey { } export class RecoveryKey extends PSKKey { + constructor(key: CryptoKey) { + super(key, createId()); + } } export async function loadBackupKeys(): Promise> { @@ -139,4 +143,47 @@ export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise(); + let jwk = new Array(); + let i; + for (i = 0; i < n; ++i) { + let keyPair = await window.crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: "P-256" }, + true, + ['sign'], + ); + let expKey = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); + + rcvKeys.push(new RecoveryKey(keyPair.privateKey)); + jwk.push(expKey); + } + + await storePSKKeys(RECOVERY, rcvKeys); + + // Download keys as file + let json = [JSON.stringify(jwk)]; + let blob1 = new Blob(json, { type: "text/plain;charset=utf-8" }); + let link = (window.URL ? URL : webkitURL).createObjectURL(blob1); + let a = document.createElement("a"); + a.download = "recoveryKeys.json"; + a.href = link; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + log.debug("Downloading recovery keys completed"); + +} + +function createId(): string{ + let enc = new TextEncoder(); + let dt = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (dt + Math.random()*16)%16 | 0; + dt = Math.floor(dt/16); + return (c=='x' ? r :(r&0x3|0x8)).toString(16); + }); + return byteArrayToBase64(enc.encode(uuid), true); } \ No newline at end of file diff --git a/src/webauthn.ts b/src/webauthn.ts index dc603ca..7112c6f 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,9 +1,9 @@ import * as CBOR from 'cbor'; -import {createCredentialId, getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; +import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import {popBackupKey, pskSetupExtensionOutput, syncBackupKeys} from "./recovery"; +import {createRecoveryKeys, popBackupKey, pskSetupExtensionOutput, syncBackupKeys} from "./recovery"; const log = getLogger('webauthn'); @@ -23,6 +23,7 @@ export const generateRegistrationKeyAndAttestation = async ( const rpID = rp.id || getDomainFromOrigin(origin); // await syncBackupKeys(); // ToDo Add own method to trigger sync + await createRecoveryKeys(5); let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); From c3a11720c9cf105ed64d32d6277f07df12274b5c Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 9 Jul 2020 12:46:31 +0200 Subject: [PATCH 10/81] Improve logging/method names --- src/recovery.ts | 27 ++++++++++-------- src/storage.ts | 73 ++----------------------------------------------- src/webauthn.ts | 2 +- 3 files changed, 19 insertions(+), 83 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 93df911..c7e60b6 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,7 +1,7 @@ import * as CBOR from 'cbor'; import {getLogger} from "./logging"; import {getCompatibleKeyFromCryptoKey} from "./crypto"; -import {base64ToByteArray, byteArrayToBase64} from "./utils"; +import { byteArrayToBase64 } from "./utils"; const log = getLogger('recovery'); @@ -36,14 +36,14 @@ class ExportKey { export class BackupKey extends PSKKey { } -export class RecoveryKey extends PSKKey { +export class ReplacementKey extends PSKKey { constructor(key: CryptoKey) { super(key, createId()); } } -export async function loadBackupKeys(): Promise> { - log.info("Loading backup keys form JSON file") +async function loadBackupKeys(): Promise> { + log.info("Loading backup keys from JSON file"); return new Promise>(function (resolve, reject) { let xhr = new XMLHttpRequest(); xhr.open("GET", chrome.extension.getURL('/recovery/backup.json'), true); @@ -89,15 +89,15 @@ async function storePSKKeys(identifier: string, psk: Array): Promise(async (res, rej) => { chrome.storage.sync.set({ [identifier]: pskJSON }, () => { if (!!chrome.runtime.lastError) { - log.info("PSK keys not stored") + log.warn(`Could not store ${identifier} keys`, pskJSON); rej(chrome.runtime.lastError); } else { - log.info("PSK keys stored") res(); } }); @@ -108,7 +108,7 @@ async function fetchPSKKeys(identifier: string): Promise> { return new Promise>(async (res, rej) => { chrome.storage.sync.get(identifier, async (resp) => { if (!!chrome.runtime.lastError) { - log.info("Could not fetch PSK keys"); + log.warn(`Could not fetch ${identifier} keys`); rej(chrome.runtime.lastError); return; } @@ -120,7 +120,6 @@ async function fetchPSKKeys(identifier: string): Promise> { let parsedKey = await parseJWK(exportedKey[i].key); pskKeys.push(new PSKKey(parsedKey, exportedKey[i].id)); } - log.info(pskKeys); res(pskKeys); }); }); @@ -128,8 +127,12 @@ async function fetchPSKKeys(identifier: string): Promise> { async function popPSKKey(identifier: string): Promise { let pskKeys = await fetchPSKKeys(identifier); + if (pskKeys.length == 0) { + throw new Error(`No ${identifier} key available to pop`); + } let key = pskKeys.pop(); await storePSKKeys(identifier, pskKeys) + log.info(`${pskKeys.length} ${identifier} keys left`); return key; } @@ -146,7 +149,7 @@ export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise(); + let rcvKeys = new Array(); let jwk = new Array(); let i; for (i = 0; i < n; ++i) { @@ -157,13 +160,13 @@ export async function createRecoveryKeys(n: number) { ); let expKey = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); - rcvKeys.push(new RecoveryKey(keyPair.privateKey)); + rcvKeys.push(new ReplacementKey(keyPair.privateKey)); jwk.push(expKey); } await storePSKKeys(RECOVERY, rcvKeys); - // Download keys as file + // Download recovery public keys as file let json = [JSON.stringify(jwk)]; let blob1 = new Blob(json, { type: "text/plain;charset=utf-8" }); let link = (window.URL ? URL : webkitURL).createObjectURL(blob1); diff --git a/src/storage.ts b/src/storage.ts index bb9e50d..9e9cf8e 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -13,73 +13,7 @@ export const keyExists = (key: string): Promise => { }); }); }; -// function hack() { -// const keyID = 'V2E2TGQ1RnFqdEJNUVFncG0rUFBxS0UvVTBzcklnTTRVeHhOQWVZU0ZaZz1Ad2ViYXV0aG4ubWU='; -// chrome.storage.sync.get(keyID, async (resp) => { -// const raw = resp[keyID]; -// console.log('breaking', raw); -// console.time(); -// const enc = new TextEncoder(); -// const payload = Uint8Array.from(atob(raw), (c) => c.charCodeAt(0)); -// const saltByteLength = payload[0]; -// const ivByteLength = payload[1]; -// const keyAlgorithmByteLength = payload[2]; -// let offset = 3; -// const salt = payload.subarray(offset, offset + saltByteLength); -// offset += saltByteLength; -// const iv = payload.subarray(offset, offset + ivByteLength); -// offset += ivByteLength; -// const keyAlgorithmBytes = payload.subarray(offset, offset + keyAlgorithmByteLength); -// offset += keyAlgorithmByteLength; -// const keyBytes = payload.subarray(offset); -// for (let i = 0; i < 10000; i++) { -// const pbkdf2Params = { -// hash: 'SHA-256', -// iterations: 100000, -// name: 'PBKDF2', -// salt, -// }; -// const derivationKey = await window.crypto.subtle.importKey( -// 'raw', -// enc.encode('' + i), -// { name: 'PBKDF2', length: 256 }, -// false, -// ['deriveBits', 'deriveKey'], -// ); -// const wrappingKey = await window.crypto.subtle.deriveKey( -// pbkdf2Params, -// derivationKey, -// { name: 'AES-GCM', length: 256 }, -// true, -// ['wrapKey', 'unwrapKey'], -// ); -// const wrapAlgorithm = { -// iv, -// name: 'AES-GCM', -// }; -// const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); -// try { -// const realPrivateKey = await window.crypto.subtle.unwrapKey( -// 'pkcs8', -// keyBytes, -// wrappingKey, -// wrapAlgorithm, -// unwrappingKeyAlgorithm, -// true, -// ['sign'], -// ); -// console.log('Success', realPrivateKey, 'in'); -// console.timeEnd(); -// return; -// } catch (e) { -// if (i % 100 === 0) { -// console.log('Testing', i, 'Running for'); -// console.timeLog(); -// } -// } -// } -// }); -// } + export const deleteKey = (key: string) => { return new Promise(async (res, _) => { chrome.storage.sync.remove(key); @@ -111,17 +45,16 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise ); }; -const log = getLogger('webauthn'); +const log = getLogger('storage'); export const fetchKey = async (key: string, pin: string): Promise => { + log.debug('Fetching key for', key); return new Promise(async (res, rej) => { chrome.storage.sync.get(key, async (resp) => { - log.info(key) if (!!chrome.runtime.lastError) { rej(chrome.runtime.lastError); return; } - log.info(resp.key) const payload = base64ToByteArray(resp[key]); const saltByteLength = payload[0]; const ivByteLength = payload[1]; diff --git a/src/webauthn.ts b/src/webauthn.ts index 7112c6f..126aa43 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -23,7 +23,7 @@ export const generateRegistrationKeyAndAttestation = async ( const rpID = rp.id || getDomainFromOrigin(origin); // await syncBackupKeys(); // ToDo Add own method to trigger sync - await createRecoveryKeys(5); + // await createRecoveryKeys(5); let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); From 24e6be59c99083fd1b457b2fec451c94e63efe26 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 9 Jul 2020 20:13:40 +0200 Subject: [PATCH 11/81] Setup recovery flow --- src/crypto.ts | 2 +- src/recovery.ts | 217 ++++++++++++++++++++++++++++++++++++++++++++++-- src/webauthn.ts | 3 +- 3 files changed, 213 insertions(+), 9 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 30def99..d0c8278 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -25,7 +25,7 @@ const ellipticNamedCurvesToCOSE: { [s: string]: number } = { 'P-256': -7, }; -interface ICOSECompatibleKey { +export interface ICOSECompatibleKey { algorithm: number; privateKey?: CryptoKey; publicKey?: CryptoKey; diff --git a/src/recovery.ts b/src/recovery.ts index c7e60b6..59ce364 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,18 +1,26 @@ import * as CBOR from 'cbor'; import {getLogger} from "./logging"; -import {getCompatibleKeyFromCryptoKey} from "./crypto"; -import { byteArrayToBase64 } from "./utils"; +import {getCompatibleKeyFromCryptoKey, ICOSECompatibleKey} from "./crypto"; +import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from "./utils"; +import {saveKey} from "./storage"; const log = getLogger('recovery'); export const PSK: string = "psk" const BACKUP: string = "backup" const RECOVERY: string = "recovery" +const DELEGATION: string = "delegation" export async function syncBackupKeys () { const bckpKeys = await loadBackupKeys(); log.info("Loaded backup keys", bckpKeys); - await storePSKKeys(BACKUP, bckpKeys) + await storePSKKeys(BACKUP, bckpKeys); +} + +export async function syncDelegation () { + const delegations = await loadDelegations(); + log.info("Loaded delegation", delegations); + await storeDelegations(delegations); } class PSKKey { @@ -36,12 +44,77 @@ class ExportKey { export class BackupKey extends PSKKey { } -export class ReplacementKey extends PSKKey { +export class RecoveryKey extends PSKKey { constructor(key: CryptoKey) { super(key, createId()); } } +class RecoveryMessage { + backupCredId: string; + delegationSignature: Uint8Array; + attestationObject: ArrayBuffer; + + constructor() { + // Dummy + } + + async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin) { + this.backupCredId = delegation.backupId; + this.delegationSignature = base64ToByteArray(delegation.signature); + + // Create attestation object for new key + const recoveryCredId = base64ToByteArray(delegation.replacementId); + const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); + log.debug('AuthData of recovery message', authData); + + this.attestationObject = CBOR.encodeCanonical({ + attStmt: new Map(), + authData: authData, + fmt: 'none', + }).buffer; + } +} + +class Delegation { + signature: string; + backupId: string; + replacementId: string; + replacementKey: JsonWebKey; + constructor(sign, backupId, jwk) { + this.backupId = backupId; + this.signature = sign; + this.replacementId = jwk.kid; + this.replacementKey = jwk; + } +} + +async function loadDelegations(): Promise> { + log.info("Loading delegations from JSON file"); + return new Promise>(function (resolve, reject) { + let xhr = new XMLHttpRequest(); + xhr.open("GET", chrome.extension.getURL('/recovery/delegation.json'), true); + xhr.onload = async function () { + let status = xhr.status; + if (status == 200) { + let rawDelegations = JSON.parse(this.response); + let i; + let del = new Array() + for (i = 0; i < rawDelegations.length; ++i) { + let rId = rawDelegations[i].public_key.kid; + let sign = rawDelegations[i].signature; + let bId = rawDelegations[i].cred_id; + del.push(new Delegation(sign, bId, rawDelegations[i].public_key)); + } + await resolve(del); + } else { + reject(status); + } + }; + xhr.send(); + }); +} + async function loadBackupKeys(): Promise> { log.info("Loading backup keys from JSON file"); return new Promise>(function (resolve, reject) { @@ -104,6 +177,23 @@ async function storePSKKeys(identifier: string, psk: Array): Promise): Promise { + let delJSON = JSON.stringify(del); + + log.debug(`Storing ${DELEGATION}`, delJSON); + + return new Promise(async (res, rej) => { + chrome.storage.sync.set({ [DELEGATION]: delJSON }, () => { + if (!!chrome.runtime.lastError) { + log.warn(`Could not store ${DELEGATION}`, del); + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); +} + async function fetchPSKKeys(identifier: string): Promise> { return new Promise>(async (res, rej) => { chrome.storage.sync.get(identifier, async (resp) => { @@ -125,6 +215,21 @@ async function fetchPSKKeys(identifier: string): Promise> { }); } +async function fetchDelegations(): Promise> { + return new Promise>(async (res, rej) => { + chrome.storage.sync.get(DELEGATION, async (resp) => { + if (!!chrome.runtime.lastError) { + log.warn(`Could not fetch ${DELEGATION}`); + rej(chrome.runtime.lastError); + return; + } + + let delegations = await JSON.parse(resp[DELEGATION]); + res(delegations); + }); + }); +} + async function popPSKKey(identifier: string): Promise { let pskKeys = await fetchPSKKeys(identifier); if (pskKeys.length == 0) { @@ -149,7 +254,7 @@ export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise(); + let rcvKeys = new Array(); let jwk = new Array(); let i; for (i = 0; i < n; ++i) { @@ -160,7 +265,7 @@ export async function createRecoveryKeys(n: number) { ); let expKey = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); - rcvKeys.push(new ReplacementKey(keyPair.privateKey)); + rcvKeys.push(new RecoveryKey(keyPair.privateKey)); jwk.push(expKey); } @@ -180,6 +285,18 @@ export async function createRecoveryKeys(n: number) { } +async function getDelegation(credentialId: string): Promise { + const del = await fetchDelegations(); + const rec = del.filter(x => x.backupId == credentialId); + return rec.length != 0 ? del[0] : null; +} + +async function getRecoveryKey(credentialId: string): Promise { + const rks = await fetchPSKKeys(RECOVERY); + const rk = rks.filter(x => x.id == credentialId); + return rk.length != 0 ? rk[0] : null; +} + function createId(): string{ let enc = new TextEncoder(); let dt = new Date().getTime(); @@ -189,4 +306,90 @@ function createId(): string{ return (c=='x' ? r :(r&0x3|0x8)).toString(16); }); return byteArrayToBase64(enc.encode(uuid), true); -} \ No newline at end of file +} + +class RecoveryOptions { + recoveryKey: RecoveryKey; + delegation: Delegation; + + constructor(rk: RecoveryKey, del: Delegation) { + this.delegation = del; + this.recoveryKey = rk; + } +} + +async function getRecoveryOptions(backupCredentialId: string): Promise { + const del = await getDelegation(backupCredentialId); + log.debug('Use delegation', del); + const rk = await getRecoveryKey(del.replacementId); + log.debug('Use recovery key', rk); + return new RecoveryOptions(rk, del); +} + + +// This function is called when recovery is needed +export const recover = async ( + origin: string, + publicKeyRequestOptions: PublicKeyCredentialRequestOptions, + pin: string, +): Promise => { + if (!publicKeyRequestOptions.allowCredentials) { + log.debug('No keys requested'); + return null; + } + + origin = 'http://localhost:9005'; // Given origin does not work! + + // For now we will only worry about the first entry + const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; + const backupCredId: ArrayBuffer = requestedCredential.id as ArrayBuffer; + const encBackupCredId = byteArrayToBase64(new Uint8Array(backupCredId), true); + log.info('Started recovery for', encBackupCredId); + + const recOps = await getRecoveryOptions(encBackupCredId); + log.debug('Recovery options', recOps); + + const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.recoveryKey.key); + const rkPubRaw = await parseJWK(recOps.delegation.replacementKey); + const rkPub = await getCompatibleKeyFromCryptoKey(rkPubRaw); + + const recMessage = (new RecoveryMessage()).init(recOps.delegation, rkPub, origin); + + // ToDo Continue here + + await saveKey(recOps.recoveryKey.id, rkPrv.privateKey, pin); + + const clientData = await rkPrv.generateClientData( + publicKeyRequestOptions.challenge as ArrayBuffer, + { + origin, + tokenBinding: { + status: 'not-supported', + }, + type: 'webauthn.get', + }, + ); + const clientDataJSON = base64ToByteArray(window.btoa(clientData)); + const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); + + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + // ToDo Create PKS Extension message and add it to !!!authData!!! + const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); + + const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); + concatData.set(authenticatorData); + concatData.set(clientDataHash, authenticatorData.length); + + const signature = await rkPrv.sign(concatData); + return { + id: recOps.recoveryKey.id, + rawId: base64ToByteArray(recOps.recoveryKey.id, true), + response: { + authenticatorData: authenticatorData.buffer, + clientDataJSON: clientDataJSON, + signature: (new Uint8Array(signature)).buffer, + userHandle: new ArrayBuffer(0), // This should be nullable + }, + type: 'public-key', + } as Credential; +}; \ No newline at end of file diff --git a/src/webauthn.ts b/src/webauthn.ts index 126aa43..908fd4e 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -3,7 +3,7 @@ import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import {createRecoveryKeys, popBackupKey, pskSetupExtensionOutput, syncBackupKeys} from "./recovery"; +import {createRecoveryKeys, popBackupKey, pskSetupExtensionOutput, syncBackupKeys, syncDelegation} from "./recovery"; const log = getLogger('webauthn'); @@ -24,6 +24,7 @@ export const generateRegistrationKeyAndAttestation = async ( // await syncBackupKeys(); // ToDo Add own method to trigger sync // await createRecoveryKeys(5); + await syncDelegation(); let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); From 9acdf6c85f7061ba08efbc9f986fa16d9ef26fcf Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 10 Jul 2020 17:21:48 +0200 Subject: [PATCH 12/81] Encoding recovery message --- src/crypto.ts | 4 +++- src/recovery.ts | 54 ++++++++++++++++++++++++++++++++++--------------- src/utils.ts | 8 +++----- src/webauthn.ts | 22 +++++++++++++++++--- 4 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index d0c8278..68eb72d 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -139,6 +139,9 @@ class ECDSA implements ICOSECompatibleKey { offset += counterToBytes(counter).length; if (!this.publicKey) { + if (extensionOutput != null) { // Extension for assertion + authenticatorData.set(extensionOutput, offset); + } return authenticatorData; } @@ -161,7 +164,6 @@ class ECDSA implements ICOSECompatibleKey { offset += encodedKey.byteLength; // Variable length for extension - // ToDo Handle extension for assertion if (extensionOutput != null) { authenticatorData.set(extensionOutput, offset); } diff --git a/src/recovery.ts b/src/recovery.ts index 59ce364..86511db 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -74,6 +74,14 @@ class RecoveryMessage { fmt: 'none', }).buffer; } + + encode(): ArrayBuffer { + return CBOR.encodeCanonical({ + attestationObject: this.attestationObject, + delegationSignature: byteArrayToBase64(this.delegationSignature), + backupCredentialId: this.backupCredId, + }).buffer; + } } class Delegation { @@ -101,10 +109,10 @@ async function loadDelegations(): Promise> { let i; let del = new Array() for (i = 0; i < rawDelegations.length; ++i) { - let rId = rawDelegations[i].public_key.kid; let sign = rawDelegations[i].signature; - let bId = rawDelegations[i].cred_id; - del.push(new Delegation(sign, bId, rawDelegations[i].public_key)); + let bId = base64ToByteArray(rawDelegations[i].cred_id, true); + const encBId = byteArrayToBase64(bId, true); + del.push(new Delegation(sign, encBId, rawDelegations[i].public_key)); } await resolve(del); } else { @@ -127,8 +135,10 @@ async function loadBackupKeys(): Promise> { let i; let bckpKeys = new Array() for (i = 0; i < jwk.length; ++i) { - let parsedKey = await parseJWK(jwk[i]); - bckpKeys.push(new BackupKey(parsedKey, jwk[i].kid)); + let parsedKey = await parseJWK(jwk[i], []); + let id = base64ToByteArray(jwk[i].kid, true); + const encId = byteArrayToBase64(id, true); + bckpKeys.push(new BackupKey(parsedKey, encId)); } await resolve(bckpKeys); } else { @@ -139,7 +149,7 @@ async function loadBackupKeys(): Promise> { }); } -async function parseJWK(jwk): Promise { +async function parseJWK(jwk, usages): Promise { return window.crypto.subtle.importKey( "jwk", jwk, @@ -148,7 +158,7 @@ async function parseJWK(jwk): Promise { namedCurve: "P-256" }, true, - [] + usages ); } @@ -207,7 +217,7 @@ async function fetchPSKKeys(identifier: string): Promise> { let pskKeys = new Array(); let i; for (i = 0; i < exportedKey.length; ++i) { - let parsedKey = await parseJWK(exportedKey[i].key); + let parsedKey = await parseJWK(exportedKey[i].key, ['sign']); pskKeys.push(new PSKKey(parsedKey, exportedKey[i].id)); } res(pskKeys); @@ -253,6 +263,10 @@ export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { + return new Uint8Array(recMsg.encode()); +} + export async function createRecoveryKeys(n: number) { let rcvKeys = new Array(); let jwk = new Array(); @@ -263,9 +277,11 @@ export async function createRecoveryKeys(n: number) { true, ['sign'], ); - let expKey = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); + let rk = new RecoveryKey(keyPair.privateKey) + let expKey: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); + expKey.kid = rk.id; - rcvKeys.push(new RecoveryKey(keyPair.privateKey)); + rcvKeys.push(rk); jwk.push(expKey); } @@ -287,13 +303,15 @@ export async function createRecoveryKeys(n: number) { async function getDelegation(credentialId: string): Promise { const del = await fetchDelegations(); - const rec = del.filter(x => x.backupId == credentialId); - return rec.length != 0 ? del[0] : null; + log.debug('Fetched delegations', del); + const rec = del.filter(x => x.backupId === credentialId); + return rec.length != 0 ? rec[0] : null; } async function getRecoveryKey(credentialId: string): Promise { const rks = await fetchPSKKeys(RECOVERY); - const rk = rks.filter(x => x.id == credentialId); + log.debug(rks); + const rk = rks.filter(x => x.id === credentialId); return rk.length != 0 ? rk[0] : null; } @@ -350,10 +368,13 @@ export const recover = async ( log.debug('Recovery options', recOps); const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.recoveryKey.key); - const rkPubRaw = await parseJWK(recOps.delegation.replacementKey); + const rkPubRaw = await parseJWK(recOps.delegation.replacementKey, []); const rkPub = await getCompatibleKeyFromCryptoKey(rkPubRaw); - const recMessage = (new RecoveryMessage()).init(recOps.delegation, rkPub, origin); + const recMessage = new RecoveryMessage(); + await recMessage.init(recOps.delegation, rkPub, origin); + log.debug('Recovery message', recMessage); + const extOutput = recMessage.encode(); // ToDo Continue here @@ -373,8 +394,9 @@ export const recover = async ( const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + // ToDo Create PKS Extension message and add it to !!!authData!!! - const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); + const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); diff --git a/src/utils.ts b/src/utils.ts index ebc67a7..362cd90 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -70,8 +70,7 @@ export function byteArrayToBase64(arr: Uint8Array, urlEncoded: boolean = false): if (urlEncoded) { return result.replace(/=/g, '') .replace(/\+/g, "-") - .replace(/\//g, "_") - .replace(/=/g, ""); + .replace(/\//g, "_"); } return result; } @@ -81,8 +80,7 @@ export function base64ToByteArray(str: string, urlEncoded: boolean = false): Uin if (urlEncoded) { rawInput = padString(rawInput) .replace(/\-/g, '+') - .replace(/_/g, '/') - .replace(/=/g, ""); + .replace(/_/g, '/'); } return Uint8Array.from(atob(rawInput), (c) => c.charCodeAt(0)); } @@ -93,4 +91,4 @@ function padString(input: string): string { result += '='; } return result; -} +} \ No newline at end of file diff --git a/src/webauthn.ts b/src/webauthn.ts index 908fd4e..18e9d8a 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -3,7 +3,14 @@ import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import {createRecoveryKeys, popBackupKey, pskSetupExtensionOutput, syncBackupKeys, syncDelegation} from "./recovery"; +import { + createRecoveryKeys, + popBackupKey, + PSK, + pskSetupExtensionOutput, recover, + syncBackupKeys, + syncDelegation +} from "./recovery"; const log = getLogger('webauthn'); @@ -22,9 +29,10 @@ export const generateRegistrationKeyAndAttestation = async ( const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - // await syncBackupKeys(); // ToDo Add own method to trigger sync + // await syncBackupKeys(); // await createRecoveryKeys(5); - await syncDelegation(); + // await syncDelegation(); + // return; let bckpKey = await popBackupKey(); log.info('Used backup key', bckpKey); @@ -84,6 +92,14 @@ export const generateKeyRequestAndAssertion = async ( origin = 'http://localhost:9005'; // Given origin does not work! + log.debug(JSON.stringify(publicKeyRequestOptions.extensions)); + const reqExt: any = publicKeyRequestOptions.extensions; + if (reqExt !== undefined) { + if (reqExt.hasOwnProperty(PSK)) { + return await recover(origin, publicKeyRequestOptions, pin); + } + } + // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; const credentialId: ArrayBuffer = requestedCredential.id as ArrayBuffer; From 79fe2852ee7330863deccc901a229d6bf4940a01 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 11 Jul 2020 16:59:39 +0200 Subject: [PATCH 13/81] Encoding adaption --- src/crypto.ts | 8 ++++---- src/recovery.ts | 28 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 68eb72d..11e09d7 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -106,9 +106,9 @@ class ECDSA implements ICOSECompatibleKey { const coseKey = await this.toCOSE(this.publicKey); encodedKey = new Uint8Array(CBOR.encode(coseKey)); authenticatorDataLength += aaguid.length - + credIdLen.byteLength + + credIdLen.length + credentialId.length - + encodedKey.byteLength; + + encodedKey.length; } if (extensionOutput != null) { @@ -153,7 +153,7 @@ class ECDSA implements ICOSECompatibleKey { // 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer. authenticatorData.set(credIdLen, offset); - offset += credIdLen.byteLength; + offset += credIdLen.length; // Variable length authenticator key ID authenticatorData.set(credentialId, offset); @@ -161,7 +161,7 @@ class ECDSA implements ICOSECompatibleKey { // Variable length public key authenticatorData.set(encodedKey, offset); - offset += encodedKey.byteLength; + offset += encodedKey.length; // Variable length for extension if (extensionOutput != null) { diff --git a/src/recovery.ts b/src/recovery.ts index 86511db..9601f3c 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -61,10 +61,12 @@ class RecoveryMessage { async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin) { this.backupCredId = delegation.backupId; - this.delegationSignature = base64ToByteArray(delegation.signature); + this.delegationSignature = base64ToByteArray(delegation.signature, true); // Create attestation object for new key const recoveryCredId = base64ToByteArray(delegation.replacementId); + + // ToDo New Credential should also contain recovery key const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); log.debug('AuthData of recovery message', authData); @@ -204,7 +206,7 @@ async function storeDelegations(del: Array): Promise { }); } -async function fetchPSKKeys(identifier: string): Promise> { +async function fetchPSKKeys(identifier: string, usages): Promise> { return new Promise>(async (res, rej) => { chrome.storage.sync.get(identifier, async (resp) => { if (!!chrome.runtime.lastError) { @@ -217,7 +219,7 @@ async function fetchPSKKeys(identifier: string): Promise> { let pskKeys = new Array(); let i; for (i = 0; i < exportedKey.length; ++i) { - let parsedKey = await parseJWK(exportedKey[i].key, ['sign']); + let parsedKey = await parseJWK(exportedKey[i].key, usages); pskKeys.push(new PSKKey(parsedKey, exportedKey[i].id)); } res(pskKeys); @@ -240,8 +242,8 @@ async function fetchDelegations(): Promise> { }); } -async function popPSKKey(identifier: string): Promise { - let pskKeys = await fetchPSKKeys(identifier); +async function popPSKKey(identifier: string, usages): Promise { + let pskKeys = await fetchPSKKeys(identifier, usages); if (pskKeys.length == 0) { throw new Error(`No ${identifier} key available to pop`); } @@ -252,19 +254,23 @@ async function popPSKKey(identifier: string): Promise { } export async function popBackupKey(): Promise { - return popPSKKey(BACKUP); + return popPSKKey(BACKUP, ['sign']); } export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); - let coseKey = await new Uint8Array(CBOR.encode(compatibleKey.toCOSE(backupKey.key))); + const coseKey = await compatibleKey.toCOSE(backupKey.key); + let encodedKey = new Uint8Array(CBOR.encode(coseKey)); + + log.debug(encodedKey); - let extOutput = new Map([[PSK, coseKey]]); + let extOutput = new Map([[PSK, encodedKey]]); return new Uint8Array(CBOR.encode(extOutput)); } async function pskRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { - return new Uint8Array(recMsg.encode()); + let extOutput = new Map([[PSK, recMsg.encode()]]); + return new Uint8Array(CBOR.encode(extOutput)); } export async function createRecoveryKeys(n: number) { @@ -309,7 +315,7 @@ async function getDelegation(credentialId: string): Promise { } async function getRecoveryKey(credentialId: string): Promise { - const rks = await fetchPSKKeys(RECOVERY); + const rks = await fetchPSKKeys(RECOVERY, ['sign']); log.debug(rks); const rk = rks.filter(x => x.id === credentialId); return rk.length != 0 ? rk[0] : null; @@ -374,7 +380,7 @@ export const recover = async ( const recMessage = new RecoveryMessage(); await recMessage.init(recOps.delegation, rkPub, origin); log.debug('Recovery message', recMessage); - const extOutput = recMessage.encode(); + const extOutput = await pskRecoveryExtensionOutput(recMessage); // ToDo Continue here From a289b77e160ae11290b0ba77c9a7dab32c520e5b Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 12 Jul 2020 17:04:43 +0200 Subject: [PATCH 14/81] Encoding --- src/crypto.ts | 6 +++++- src/recovery.ts | 35 +++++++++++++++++++++++++++++------ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 11e09d7..f1fd7cb 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -38,7 +38,11 @@ export interface ICOSECompatibleKey { class ECDSA implements ICOSECompatibleKey { public static async fromKey(key: CryptoKey): Promise { - return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], key); + if (key.type === "public") { + return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], null, key); + } else { + return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], key); + } } public static async fromCOSEAlgorithm(algorithm: number): Promise { diff --git a/src/recovery.ts b/src/recovery.ts index 9601f3c..cc21a0b 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -53,13 +53,16 @@ export class RecoveryKey extends PSKKey { class RecoveryMessage { backupCredId: string; delegationSignature: Uint8Array; - attestationObject: ArrayBuffer; + pubKey: Uint8Array; + + //attestationObject: ArrayBuffer; + //clientDataJSON: ArrayBuffer constructor() { // Dummy } - async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin) { + async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin: string, challenge: ArrayBuffer) { this.backupCredId = delegation.backupId; this.delegationSignature = base64ToByteArray(delegation.signature, true); @@ -67,19 +70,36 @@ class RecoveryMessage { const recoveryCredId = base64ToByteArray(delegation.replacementId); // ToDo New Credential should also contain recovery key + // ToDo Use valid attestation response const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); log.debug('AuthData of recovery message', authData); - this.attestationObject = CBOR.encodeCanonical({ + console.log(rkPub.publicKey) + console.log(rkPub.privateKey) + + const coseKey = await rkPub.toCOSE(rkPub.publicKey); + this.pubKey = new Uint8Array(CBOR.encode(coseKey)); + + /*this.attestationObject = CBOR.encodeCanonical({ attStmt: new Map(), authData: authData, fmt: 'none', }).buffer; + + + const clientData = await rkPub.generateClientData( + challenge, + { origin, type: 'webauthn.create' }, + ); + + this.clientDataJSON = base64ToByteArray(window.btoa(clientData), true);*/ } encode(): ArrayBuffer { return CBOR.encodeCanonical({ - attestationObject: this.attestationObject, + /*clientDataJSON: this.clientDataJSON, + attestationObject: this.attestationObject,*/ + publicKey: this.pubKey, delegationSignature: byteArrayToBase64(this.delegationSignature), backupCredentialId: this.backupCredId, }).buffer; @@ -362,6 +382,8 @@ export const recover = async ( return null; } + await syncDelegation(); + origin = 'http://localhost:9005'; // Given origin does not work! // For now we will only worry about the first entry @@ -374,11 +396,13 @@ export const recover = async ( log.debug('Recovery options', recOps); const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.recoveryKey.key); + log.debug('prv', rkPrv.privateKey); const rkPubRaw = await parseJWK(recOps.delegation.replacementKey, []); const rkPub = await getCompatibleKeyFromCryptoKey(rkPubRaw); + log.debug('pub', rkPub.publicKey); const recMessage = new RecoveryMessage(); - await recMessage.init(recOps.delegation, rkPub, origin); + await recMessage.init(recOps.delegation, rkPub, origin, publicKeyRequestOptions.challenge as ArrayBuffer); log.debug('Recovery message', recMessage); const extOutput = await pskRecoveryExtensionOutput(recMessage); @@ -401,7 +425,6 @@ export const recover = async ( const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - // ToDo Create PKS Extension message and add it to !!!authData!!! const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); From cead4acb0382f4ae96545a3cfce701d03d7fc955 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 13 Jul 2020 19:28:29 +0200 Subject: [PATCH 15/81] Add attestation object during recovery --- src/recovery.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index cc21a0b..66b0990 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -54,9 +54,8 @@ class RecoveryMessage { backupCredId: string; delegationSignature: Uint8Array; pubKey: Uint8Array; - - //attestationObject: ArrayBuffer; - //clientDataJSON: ArrayBuffer + attestationObject: Uint8Array; + clientDataJSON: Uint8Array; constructor() { // Dummy @@ -74,34 +73,33 @@ class RecoveryMessage { const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); log.debug('AuthData of recovery message', authData); - console.log(rkPub.publicKey) - console.log(rkPub.privateKey) - const coseKey = await rkPub.toCOSE(rkPub.publicKey); this.pubKey = new Uint8Array(CBOR.encode(coseKey)); - /*this.attestationObject = CBOR.encodeCanonical({ + this.attestationObject = CBOR.encodeCanonical({ attStmt: new Map(), authData: authData, fmt: 'none', - }).buffer; + }); const clientData = await rkPub.generateClientData( challenge, { origin, type: 'webauthn.create' }, ); + this.clientDataJSON = base64ToByteArray(window.btoa(clientData), true); - this.clientDataJSON = base64ToByteArray(window.btoa(clientData), true);*/ } encode(): ArrayBuffer { return CBOR.encodeCanonical({ - /*clientDataJSON: this.clientDataJSON, - attestationObject: this.attestationObject,*/ publicKey: this.pubKey, delegationSignature: byteArrayToBase64(this.delegationSignature), backupCredentialId: this.backupCredId, + authAttData: { + clientDataJSON: this.clientDataJSON, + attestationObject: this.attestationObject + }, }).buffer; } } From b96978db71e9950113d921d83f1468e83f74e004 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 15 Jul 2020 19:59:58 +0200 Subject: [PATCH 16/81] Add upload for delegation and backup --- dist/chromium/popup.html | 12 ++++++++ src/background.ts | 31 ++++++++++++++++---- src/popup.ts | 47 ++++++++++++++++++++++++++++++ src/recovery.ts | 63 ++++++++++++++++------------------------ src/webauthn.ts | 5 ++-- 5 files changed, 112 insertions(+), 46 deletions(-) diff --git a/dist/chromium/popup.html b/dist/chromium/popup.html index 29e9940..a9503d1 100644 --- a/dist/chromium/popup.html +++ b/dist/chromium/popup.html @@ -17,6 +17,18 @@ +
+ PSK Options +
+ Backup File Sync: +
+
+ Delegation File Sync: +
+
+ Download recovery keys +
+
\ No newline at end of file diff --git a/src/background.ts b/src/background.ts index 27ab8c2..6ed9dd2 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,7 +1,8 @@ -import { disabledIcons, enabledIcons } from './constants'; -import { getLogger } from './logging'; -import { getOriginFromUrl, webauthnParse, webauthnStringify } from './utils'; -import { generateKeyRequestAndAssertion, generateRegistrationKeyAndAttestation } from './webauthn'; +import {disabledIcons, enabledIcons} from './constants'; +import {getLogger} from './logging'; +import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; +import {generateKeyRequestAndAssertion, generateRegistrationKeyAndAttestation} from './webauthn'; +import {syncBackupKeys, syncDelegation} from "./recovery"; const log = getLogger('background'); @@ -33,6 +34,18 @@ const requestPin = async (tabId: number, origin: string, newPin: boolean = true) return pin; }; +const syncBackup = async (backupContent) => { + console.log('Sync Backup called'); + + await syncBackupKeys(backupContent); +}; + +const syncDel = async (delegationContent) => { + console.log('Sync Delegation called'); + + await syncDelegation(delegationContent); +}; + const create = async (msg, sender: chrome.runtime.MessageSender) => { if (!sender.tab || !sender.tab.id) { log.debug('received create event without a tab ID'); @@ -66,16 +79,16 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { const sign = async (msg, sender: chrome.runtime.MessageSender) => { const opts = webauthnParse(msg.options); + const origin = getOriginFromUrl(sender.url); const pin = await requestPin(sender.tab.id, origin); try { const credential = await generateKeyRequestAndAssertion(origin, opts.publicKey, `${pin}`); - const authenticatedResponseData = { + return { credential: webauthnStringify(credential), requestID: msg.requestID, type: 'sign_response', }; - return authenticatedResponseData; } catch (e) { if (e instanceof DOMException) { const { code, message, name } = e; @@ -103,6 +116,12 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { delete (pinProtectedCallbacks[msg.tabId]); } break; + case 'syncBackup': + syncBackup(msg.backup).then(() => alert("Backup file processed")); + break; + case 'syncDelegation': + syncDel(msg.delegation).then(() => alert("Delegation file processed")); + break; default: sendResponse(null); } diff --git a/src/popup.ts b/src/popup.ts index 8df0dc7..c14ef2e 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -3,12 +3,59 @@ import { getLogger } from './logging'; const log = getLogger('popup'); + $(() => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => { const currentTab = tabs.find((t) => !!t.id); if (!currentTab) { return; } + + $('#delegationFile').on('change', function(evt: Event) { + const files = (evt.target).files; // FileList object + + // use the 1st file from the list + const f = files[0]; + + const reader = new FileReader(); + + // Closure to capture the file information. + reader.onload = (function(theFile) { + return function(e) { + chrome.runtime.sendMessage({ + delegation: e.target.result, + type: 'syncDelegation', + }); + }; + })(f); + + // Read in the image file as a data URL. + reader.readAsText(f); + }); + $('#backupFile').on('change', function(evt: Event) { + evt.preventDefault(); + const files = (evt.target).files; // FileList object + + // use the 1st file from the list + const f = files[0]; + + const reader = new FileReader(); + + // Closure to capture the file information. + reader.onload = (function(theFile) { + return function(e) { + chrome.runtime.sendMessage({ + backup: e.target.result, + type: 'syncBackup', + }); + }; + })(f); + + // Read in the image file as a data URL. + reader.readAsText(f); + }); + + const tabKey = `tab-${currentTab.id}`; chrome.storage.local.get([tabKey], (result) => { log.debug('got storage results', result); diff --git a/src/recovery.ts b/src/recovery.ts index 66b0990..af7ba4b 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -11,16 +11,32 @@ const BACKUP: string = "backup" const RECOVERY: string = "recovery" const DELEGATION: string = "delegation" -export async function syncBackupKeys () { - const bckpKeys = await loadBackupKeys(); +export async function syncBackupKeys (content) { + let jwk = JSON.parse(content); + let i; + let bckpKeys = new Array() + for (i = 0; i < jwk.length; ++i) { + let parsedKey = await parseJWK(jwk[i], []); + let id = base64ToByteArray(jwk[i].kid, true); + const encId = byteArrayToBase64(id, true); + bckpKeys.push(new BackupKey(parsedKey, encId)); + } log.info("Loaded backup keys", bckpKeys); await storePSKKeys(BACKUP, bckpKeys); } -export async function syncDelegation () { - const delegations = await loadDelegations(); - log.info("Loaded delegation", delegations); - await storeDelegations(delegations); +export async function syncDelegation (content) { + let rawDelegations = JSON.parse(content); + let i; + let del = new Array() + for (i = 0; i < rawDelegations.length; ++i) { + let sign = rawDelegations[i].signature; + let bId = base64ToByteArray(rawDelegations[i].cred_id, true); + const encBId = byteArrayToBase64(bId, true); + del.push(new Delegation(sign, encBId, rawDelegations[i].public_key)); + } + log.info("Loaded delegation", del); + await storeDelegations(del); } class PSKKey { @@ -143,32 +159,6 @@ async function loadDelegations(): Promise> { }); } -async function loadBackupKeys(): Promise> { - log.info("Loading backup keys from JSON file"); - return new Promise>(function (resolve, reject) { - let xhr = new XMLHttpRequest(); - xhr.open("GET", chrome.extension.getURL('/recovery/backup.json'), true); - xhr.onload = async function () { - let status = xhr.status; - if (status == 200) { - let jwk = JSON.parse(this.response); - let i; - let bckpKeys = new Array() - for (i = 0; i < jwk.length; ++i) { - let parsedKey = await parseJWK(jwk[i], []); - let id = base64ToByteArray(jwk[i].kid, true); - const encId = byteArrayToBase64(id, true); - bckpKeys.push(new BackupKey(parsedKey, encId)); - } - await resolve(bckpKeys); - } else { - reject(status); - } - }; - xhr.send(); - }); -} - async function parseJWK(jwk, usages): Promise { return window.crypto.subtle.importKey( "jwk", @@ -380,9 +370,8 @@ export const recover = async ( return null; } - await syncDelegation(); - - origin = 'http://localhost:9005'; // Given origin does not work! + //origin = 'http://localhost:9005'; // Given origin does not work! + log.debug('origin', origin) // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; @@ -404,8 +393,6 @@ export const recover = async ( log.debug('Recovery message', recMessage); const extOutput = await pskRecoveryExtensionOutput(recMessage); - // ToDo Continue here - await saveKey(recOps.recoveryKey.id, rkPrv.privateKey, pin); const clientData = await rkPrv.generateClientData( @@ -430,7 +417,7 @@ export const recover = async ( concatData.set(clientDataHash, authenticatorData.length); const signature = await rkPrv.sign(concatData); - return { + return { // ToDo Make getClientExtensionResults work id: recOps.recoveryKey.id, rawId: base64ToByteArray(recOps.recoveryKey.id, true), response: { diff --git a/src/webauthn.ts b/src/webauthn.ts index 18e9d8a..f792578 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -68,7 +68,7 @@ export const generateRegistrationKeyAndAttestation = async ( log.debug('send attestation'); return { - getClientExtensionResults: () => ({}), + getClientExtensionResults: () => ({}), // ToDo Put PSK extension data id: encCredId, rawId: credentialId, response: { @@ -90,7 +90,8 @@ export const generateKeyRequestAndAssertion = async ( return null; } - origin = 'http://localhost:9005'; // Given origin does not work! + // origin = 'http://localhost:9005'; // Given origin does not work! + log.debug('origin', origin) log.debug(JSON.stringify(publicKeyRequestOptions.extensions)); const reqExt: any = publicKeyRequestOptions.extensions; From 6dd6ede6c7f5fec1e0d1ebb50caf8a13d070b9ed Mon Sep 17 00:00:00 2001 From: Blobonat Date: Thu, 16 Jul 2020 09:56:00 +0200 Subject: [PATCH 17/81] Bugfix/webauthn conformity (#1) * Fix WebAuthn registration and authentication flow * Fix origin during authentication --- dist/chromium/manifest.json | 5 ++- package.json | 2 + src/background.ts | 7 +-- src/crypto.ts | 51 ++++++++++++--------- src/utils.ts | 10 +++-- src/webauthn.ts | 90 ++++++++++++++++++++++--------------- 6 files changed, 100 insertions(+), 65 deletions(-) diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index 744560f..0f8b3cf 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -2,13 +2,14 @@ "manifest_version": 2, "name": "CKey", "description": "A Chrome Extension that emulates a Hardware Authentication Device", - "version": "1.0.2", + "version": "1.0.4", "minimum_chrome_version": "36.0.1985.18", "content_scripts": [ { "all_frames": true, "matches": [ - "https://*/*" + "https://*/*", + "http://localhost/*" ], "exclude_matches": [ "https://*/*.xml" diff --git a/package.json b/package.json index c78feeb..f48b9fd 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "@types/jquery": "^3.3.31", "@types/loglevel": "^1.6.3", "@types/webappsec-credential-management": "^0.3.11", + "asn1js": "^2.0.26", + "bn.js": "^5.1.2", "cbor": "^4.3.0", "jquery": "^3.4.1", "loglevel": "^1.6.6", diff --git a/src/background.ts b/src/background.ts index b378a08..16c3670 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,7 +1,7 @@ import { disabledIcons, enabledIcons } from './constants'; import { getLogger } from './logging'; import { getOriginFromUrl, webauthnParse, webauthnStringify } from './utils'; -import { generateKeyRequestAndAttestation, generateRegistrationKeyAndAttestation } from './webauthn'; +import { generateAssertionResponse, generateAttestationResponse } from './webauthn'; const log = getLogger('background'); @@ -44,7 +44,7 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { try { const opts = webauthnParse(msg.options); - const credential = await generateRegistrationKeyAndAttestation( + const credential = await generateAttestationResponse( origin, opts.publicKey, `${pin}`, @@ -66,10 +66,11 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { const sign = async (msg, sender: chrome.runtime.MessageSender) => { const opts = webauthnParse(msg.options); + const origin = getOriginFromUrl(sender.url); const pin = await requestPin(sender.tab.id, origin); try { - const credential = await generateKeyRequestAndAttestation(origin, opts.publicKey, `${pin}`); + const credential = await generateAssertionResponse(origin, opts.publicKey, `${pin}`); const authenticatedResponseData = { credential: webauthnStringify(credential), requestID: msg.requestID, diff --git a/src/crypto.ts b/src/crypto.ts index 2b2cef0..74de1e8 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,12 +1,14 @@ import * as CBOR from 'cbor'; import { getLogger } from './logging'; import { base64ToByteArray, byteArrayToBase64 } from './utils'; +import * as asn1 from 'asn1.js'; +import { BN } from 'bn.js'; const log = getLogger('crypto'); // Generated with pseudo random values via // https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues -export const CKEY_ID = new Uint8Array([ +const CKEY_ID = new Uint8Array([ 194547236, 76082241, 3628762690, 4137210381, 1214244733, 1205845608, 840015201, 3897052717, 4072880437, 4027233456, 675224361, 2305433287, @@ -29,14 +31,10 @@ function counterToBytes(c: number): Uint8Array { const coseEllipticCurveNames: { [s: number]: string } = { 1: 'SHA-256', - 2: 'SHA-384', - 3: 'SHA-512', }; const ellipticNamedCurvesToCOSE: { [s: string]: number } = { 'P-256': -7, - 'P-384': -35, - 'P-512': -36, }; interface ICOSECompatibleKey { @@ -44,8 +42,8 @@ interface ICOSECompatibleKey { privateKey: CryptoKey; publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; - generateAuthenticatorData(rpID: string, counter: number): Promise; - sign(clientData: string): Promise; + generateAuthenticatorData(rpID: string, counter: number, credentialID: Uint8Array): Promise; + sign(data: Uint8Array): Promise; } class ECDSA implements ICOSECompatibleKey { @@ -102,7 +100,7 @@ class ECDSA implements ICOSECompatibleKey { }); } - public async generateAuthenticatorData(rpID: string, counter: number): Promise { + public async generateAuthenticatorData(rpID: string, counter: number, credentialID: Uint8Array): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); @@ -116,13 +114,13 @@ class ECDSA implements ICOSECompatibleKey { aaguid = CKEY_ID.slice(0, 16); // 16-bit unsigned big-endian integer. credIdLen = new Uint8Array(2); - credIdLen[0] = (CKEY_ID.length >> 8) & 0xff; - credIdLen[1] = CKEY_ID.length & 0xff; + credIdLen[0] = (credentialID.length >> 8) & 0xff; + credIdLen[1] = credentialID.length & 0xff; const coseKey = await this.toCOSE(this.publicKey); encodedKey = new Uint8Array(CBOR.encode(coseKey)); authenticatorDataLength += aaguid.length + credIdLen.byteLength - + CKEY_ID.length + + credentialID.length + encodedKey.byteLength; } @@ -139,8 +137,8 @@ class ECDSA implements ICOSECompatibleKey { if (this.publicKey) { // attestation flag goes on the 7th bit (from the right) authenticatorData[rpIdHash.length] |= (1 << 6); - offset++; } + offset++; // 4 bytes for the counter. big-endian uint32 // https://www.w3.org/TR/webauthn/#signature-counter @@ -155,13 +153,13 @@ class ECDSA implements ICOSECompatibleKey { authenticatorData.set(aaguid, offset); offset += aaguid.length; - // 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer. + // 2 bytes for the credential ID length. 16-bit unsigned big-endian integer. authenticatorData.set(credIdLen, offset); offset += credIdLen.byteLength; - // Variable length authenticator key ID - authenticatorData.set(CKEY_ID, offset); - offset += CKEY_ID.length; + // Variable length credential ID + authenticatorData.set(credentialID, offset); + offset += credentialID.length; // Variable length public key authenticatorData.set(encodedKey, offset); @@ -169,15 +167,28 @@ class ECDSA implements ICOSECompatibleKey { return authenticatorData; } - public async sign(data: string): Promise { + public async sign(data: Uint8Array): Promise { if (!this.privateKey) { throw new Error('no private key available for signing'); } - return window.crypto.subtle.sign( + const rawSign = await window.crypto.subtle.sign( this.getKeyParams(), this.privateKey, - new TextEncoder().encode(data), + data, ); + + const rawSignBuf = new Buffer(rawSign); + + // Credit to: https://stackoverflow.com/a/39651457/5333936 + const EcdsaDerSig = asn1.define('ECPrivateKey', function() { + return this.seq().obj( + this.key('r').int(), + this.key('s').int() + ); + }); + const r = new BN(rawSignBuf.slice(0, 32).toString('hex'), 16, 'be'); + const s = new BN(rawSignBuf.slice(32).toString('hex'), 16, 'be'); + return EcdsaDerSig.encode({r, s}, 'der'); } private getKeyParams(): EcdsaParams { @@ -203,8 +214,6 @@ class ECDSA implements ICOSECompatibleKey { const defaultPKParams = { alg: -7, type: 'public-key' }; const coseAlgorithmToKeyName = { [-7]: 'ECDSA', - [-35]: 'ECDSA', - [-36]: 'ECDSA', }; export const getCompatibleKey = (pkParams: PublicKeyCredentialParameters[]): Promise => { diff --git a/src/utils.ts b/src/utils.ts index 43aedc8..22c997e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,8 +69,9 @@ export function byteArrayToBase64(arr: Uint8Array, urlEncoded: boolean = false): const result = btoa(String.fromCharCode(...arr)); if (urlEncoded) { return result.replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } return result; } @@ -79,8 +80,9 @@ export function base64ToByteArray(str: string, urlEncoded: boolean = false): Uin let rawInput = str; if (urlEncoded) { rawInput = padString(rawInput) - .replace(/\-/g, '+') - .replace(/_/g, '/'); + .replace(/-/g, '+') + .replace(/_/g, '/') + .replace(/=/g, ""); } return Uint8Array.from(atob(rawInput), (c) => c.charCodeAt(0)); } diff --git a/src/webauthn.ts b/src/webauthn.ts index 9d0dd94..eda8472 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -6,53 +6,48 @@ import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './uti const log = getLogger('webauthn'); -export const generateRegistrationKeyAndAttestation = async ( +export const generateAttestationResponse = async ( origin: string, publicKeyCreationOptions: PublicKeyCredentialCreationOptions, pin: string, ): Promise => { - if (publicKeyCreationOptions.attestation === 'direct') { - log.warn('We are being requested to create a key with "direct" attestation'); - log.warn(`We can only perform self-attestation, therefore we will not be provisioning any keys`); + if (publicKeyCreationOptions.attestation !== 'none') { + log.warn(`We are being requested to create a credential with ${publicKeyCreationOptions.attestation} attestation`); + log.warn(`We can only perform none attestation, therefore we will not be provisioning any credentials`); return null; } const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - const user = publicKeyCreationOptions.user; - const userID = byteArrayToBase64(new Uint8Array(user.id as ArrayBuffer)); - const keyID = window.btoa(`${userID}@${rpID}`); + const credId = createCredentialId(); + const encCredId = byteArrayToBase64(credId, true); // First check if there is already a key for this rp ID - if (await keyExists(keyID)) { - throw new Error(`key with id ${keyID} already exists`); + if (await keyExists(encCredId)) { + throw new Error(`credential with id ${encCredId} already exists`); } - log.debug('key ID', keyID); + log.debug('key ID', encCredId); const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); // TODO Increase key counter - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId); const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, ); - const signature = await compatibleKey.sign(clientData); const attestationObject = CBOR.encodeCanonical({ - attStmt: { - alg: compatibleKey.algorithm, - sig: signature, - }, + attStmt: new Map(), authData: authenticatorData, - fmt: 'packed', + fmt: 'none', }).buffer; - // Now that we have built all we need, let's save the key - await saveKey(keyID, compatibleKey.privateKey, pin); + // Now that we have built all we need, let's save the private key + await saveKey(encCredId, compatibleKey.privateKey, pin); return { getClientExtensionResults: () => ({}), - id: keyID, - rawId: base64ToByteArray(keyID), + id: encCredId, + rawId: credId, response: { attestationObject, clientDataJSON: base64ToByteArray(window.btoa(clientData)), @@ -61,25 +56,31 @@ export const generateRegistrationKeyAndAttestation = async ( } as PublicKeyCredential; }; -export const generateKeyRequestAndAttestation = async ( +export const generateAssertionResponse = async ( origin: string, publicKeyRequestOptions: PublicKeyCredentialRequestOptions, pin: string, ): Promise => { if (!publicKeyRequestOptions.allowCredentials) { - log.debug('No keys requested'); + log.debug('No credentials requested'); return null; } + // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; - const keyIDArray: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const keyID = byteArrayToBase64(new Uint8Array(keyIDArray)); - const key = await fetchKey(keyID, pin); + const credId: ArrayBuffer = requestedCredential.id as ArrayBuffer; + const endCredId = byteArrayToBase64(new Uint8Array(credId), true); + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + + log.debug('credential ID', endCredId); + + const key = await fetchKey(endCredId, pin); if (!key) { - throw new Error(`key with id ${keyID} not found`); + throw new Error(`credentials with id ${endCredId} not found`); } const compatibleKey = await getCompatibleKeyFromCryptoKey(key); + const clientData = await compatibleKey.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, { @@ -87,21 +88,40 @@ export const generateKeyRequestAndAttestation = async ( tokenBinding: { status: 'not-supported', }, - type: 'webauthn.create', + type: 'webauthn.get', }, ); - const signature = await compatibleKey.sign(clientData); - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0); + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array()); + const clientDataJSON = base64ToByteArray(window.btoa(clientData)); + const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); + + const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); + concatData.set(authenticatorData); + concatData.set(clientDataHash, authenticatorData.length); + + + const signature = await compatibleKey.sign(concatData); + return { - id: keyID, - rawId: keyIDArray, + id: endCredId, + rawId: credId, response: { authenticatorData: authenticatorData.buffer, - clientDataJSON: base64ToByteArray(window.btoa(clientData)), - signature, + clientDataJSON: clientDataJSON, + signature: (new Uint8Array(signature)).buffer, userHandle: new ArrayBuffer(0), // This should be nullable }, type: 'public-key', } as Credential; }; + +function createCredentialId(): Uint8Array{ + let enc = new TextEncoder(); + let dt = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (dt + Math.random()*16)%16 | 0; + dt = Math.floor(dt/16); + return (c=='x' ? r :(r&0x3|0x8)).toString(16); + }); + return base64ToByteArray(byteArrayToBase64(enc.encode(uuid), true), true); +} From ad08bba9558b925701b097b49b88763a65a79b1e Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 16 Jul 2020 18:25:07 +0200 Subject: [PATCH 18/81] Finish basic recovery flow --- dist/chromium/popup.html | 2 +- src/background.ts | 11 ++++++++++- src/popup.ts | 8 ++++++++ src/recovery.ts | 29 +---------------------------- 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/dist/chromium/popup.html b/dist/chromium/popup.html index a9503d1..adadc38 100644 --- a/dist/chromium/popup.html +++ b/dist/chromium/popup.html @@ -26,7 +26,7 @@ Delegation File Sync:
- Download recovery keys +
diff --git a/src/background.ts b/src/background.ts index 6ed9dd2..7140b8d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -2,7 +2,7 @@ import {disabledIcons, enabledIcons} from './constants'; import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {generateKeyRequestAndAssertion, generateRegistrationKeyAndAttestation} from './webauthn'; -import {syncBackupKeys, syncDelegation} from "./recovery"; +import {createRecoveryKeys, syncBackupKeys, syncDelegation} from "./recovery"; const log = getLogger('background'); @@ -46,6 +46,12 @@ const syncDel = async (delegationContent) => { await syncDelegation(delegationContent); }; +const recovery = async (n) => { + console.log('Create recovery keys called') + + await createRecoveryKeys(n); +} + const create = async (msg, sender: chrome.runtime.MessageSender) => { if (!sender.tab || !sender.tab.id) { log.debug('received create event without a tab ID'); @@ -122,6 +128,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'syncDelegation': syncDel(msg.delegation).then(() => alert("Delegation file processed")); break; + case 'recovery': + recovery(msg.amount).then(() => alert("Creating recovery keys finished")) + break; default: sendResponse(null); } diff --git a/src/popup.ts b/src/popup.ts index c14ef2e..7071ff3 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -55,6 +55,14 @@ $(() => { reader.readAsText(f); }); + $('#recovery').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + amount: 5, // ToDo Read real input + type: 'recovery', + }); + }); + const tabKey = `tab-${currentTab.id}`; chrome.storage.local.get([tabKey], (result) => { diff --git a/src/recovery.ts b/src/recovery.ts index af7ba4b..c808897 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -85,7 +85,6 @@ class RecoveryMessage { const recoveryCredId = base64ToByteArray(delegation.replacementId); // ToDo New Credential should also contain recovery key - // ToDo Use valid attestation response const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); log.debug('AuthData of recovery message', authData); @@ -133,32 +132,6 @@ class Delegation { } } -async function loadDelegations(): Promise> { - log.info("Loading delegations from JSON file"); - return new Promise>(function (resolve, reject) { - let xhr = new XMLHttpRequest(); - xhr.open("GET", chrome.extension.getURL('/recovery/delegation.json'), true); - xhr.onload = async function () { - let status = xhr.status; - if (status == 200) { - let rawDelegations = JSON.parse(this.response); - let i; - let del = new Array() - for (i = 0; i < rawDelegations.length; ++i) { - let sign = rawDelegations[i].signature; - let bId = base64ToByteArray(rawDelegations[i].cred_id, true); - const encBId = byteArrayToBase64(bId, true); - del.push(new Delegation(sign, encBId, rawDelegations[i].public_key)); - } - await resolve(del); - } else { - reject(status); - } - }; - xhr.send(); - }); -} - async function parseJWK(jwk, usages): Promise { return window.crypto.subtle.importKey( "jwk", @@ -262,7 +235,7 @@ async function popPSKKey(identifier: string, usages): Promise { } export async function popBackupKey(): Promise { - return popPSKKey(BACKUP, ['sign']); + return popPSKKey(BACKUP, []); } export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { From 5f018cdd10cae3dacb19651b2c8a945be24d02a6 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 19 Jul 2020 18:17:10 +0200 Subject: [PATCH 19/81] Refactoring --- src/background.ts | 6 +- src/recovery.ts | 306 ++++++++++++++++++++-------------------------- src/storage.ts | 64 ++++++++-- src/webauthn.ts | 82 +++++++------ 4 files changed, 230 insertions(+), 228 deletions(-) diff --git a/src/background.ts b/src/background.ts index 7140b8d..a649c7e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,7 +1,7 @@ import {disabledIcons, enabledIcons} from './constants'; import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; -import {generateKeyRequestAndAssertion, generateRegistrationKeyAndAttestation} from './webauthn'; +import {processCredentialRequest, processCredentialCreation} from './webauthn'; import {createRecoveryKeys, syncBackupKeys, syncDelegation} from "./recovery"; const log = getLogger('background'); @@ -63,7 +63,7 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { try { const opts = webauthnParse(msg.options); - const credential = await generateRegistrationKeyAndAttestation( + const credential = await processCredentialCreation( origin, opts.publicKey, `${pin}`, @@ -89,7 +89,7 @@ const sign = async (msg, sender: chrome.runtime.MessageSender) => { const pin = await requestPin(sender.tab.id, origin); try { - const credential = await generateKeyRequestAndAssertion(origin, opts.publicKey, `${pin}`); + const credential = await processCredentialRequest(origin, opts.publicKey, `${pin}`); return { credential: webauthnStringify(credential), requestID: msg.requestID, diff --git a/src/recovery.ts b/src/recovery.ts index c808897..717fc8c 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,72 +1,130 @@ import * as CBOR from 'cbor'; -import {getLogger} from "./logging"; -import {getCompatibleKeyFromCryptoKey, ICOSECompatibleKey} from "./crypto"; -import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from "./utils"; -import {saveKey} from "./storage"; +import {getLogger} from './logging'; +import {getCompatibleKeyFromCryptoKey, ICOSECompatibleKey} from './crypto'; +import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; +import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; const log = getLogger('recovery'); -export const PSK: string = "psk" -const BACKUP: string = "backup" -const RECOVERY: string = "recovery" -const DELEGATION: string = "delegation" +export const PSK: string = 'psk' + +export type ExportContainerType = string +const BACKUP: ExportContainerType = 'backup' +const RECOVERY: ExportContainerType = 'recovery' +const DELEGATION: ExportContainerType = 'delegation' export async function syncBackupKeys (content) { - let jwk = JSON.parse(content); + const jwk = JSON.parse(content); let i; - let bckpKeys = new Array() + const container = new Array(); for (i = 0; i < jwk.length; ++i) { - let parsedKey = await parseJWK(jwk[i], []); - let id = base64ToByteArray(jwk[i].kid, true); + const parsedKey = await parseJWK(jwk[i], []); + const id = base64ToByteArray(jwk[i].kid, true); const encId = byteArrayToBase64(id, true); - bckpKeys.push(new BackupKey(parsedKey, encId)); + const bckpKey = new BackupKey(parsedKey, encId); + const expBckpKey = await bckpKey.export(); + container.push(expBckpKey); } - log.info("Loaded backup keys", bckpKeys); - await storePSKKeys(BACKUP, bckpKeys); + log.debug('Loaded backup keys', container); + + await saveExportContainer(BACKUP, container); } export async function syncDelegation (content) { - let rawDelegations = JSON.parse(content); + const rawDelegations = JSON.parse(content); let i; - let del = new Array() + const container = new Array(); for (i = 0; i < rawDelegations.length; ++i) { - let sign = rawDelegations[i].signature; - let bId = base64ToByteArray(rawDelegations[i].cred_id, true); + const sign = rawDelegations[i].signature; + const bId = base64ToByteArray(rawDelegations[i].cred_id, true); const encBId = byteArrayToBase64(bId, true); - del.push(new Delegation(sign, encBId, rawDelegations[i].public_key)); + const del = new Delegation(sign, encBId, rawDelegations[i].public_key); + container.push(del.export()); } - log.info("Loaded delegation", del); - await storeDelegations(del); + log.debug("Loaded delegation", container); + await saveExportContainer(DELEGATION, container); } -class PSKKey { +export class ExportContainer { + id: string; + payload: string; + + constructor(id: string, payload: string) { + this.id = id; + this.payload = payload; + } +} + +export class BackupKey { key: CryptoKey; id: string; + constructor(key: CryptoKey, id: string) { this.key = key; this.id = id; } + + async export(): Promise { + const jwk = await window.crypto.subtle.exportKey("jwk", this.key); + const encJWK = JSON.stringify(jwk); + return new ExportContainer(this.id, encJWK); + } + static async import(kx: ExportContainer): Promise { + const rawKey = JSON.parse(kx.payload); + const key = await parseJWK(rawKey, []); + return new BackupKey(key, kx.id); + } } -class ExportKey { - key: JsonWebKey; +export class RecoveryKey { + key: CryptoKey; id: string; - constructor(key: JsonWebKey, id: string) { + backupKey: BackupKey; + + constructor(key: CryptoKey, backupKey: BackupKey) { + this.id = backupKey.id; + this.backupKey = backupKey; this.key = key; - this.id = id; } -} -export class BackupKey extends PSKKey { + async export(): Promise { + const parsedKey = await window.crypto.subtle.exportKey("jwk", this.key); + const expBackupKey = await this.backupKey.export(); + const rawJSON = {parsedKey: parsedKey, parsedBackupKey: expBackupKey}; + + return new ExportContainer(this.id, JSON.stringify(rawJSON)); + } + + static async import(kx: ExportContainer): Promise { + const json = JSON.parse(kx.payload); + const key = await parseJWK(json.parsedKey, ['sign']); + const backupKey = await BackupKey.import(json.parsedBackupKey); + + return new RecoveryKey(key, backupKey); + } } -export class RecoveryKey extends PSKKey { - constructor(key: CryptoKey) { - super(key, createId()); +class Delegation { + signature: string; + backupId: string; + replacementId: string; + replacementKey: JsonWebKey; + constructor(sign, backupId, jwk) { + this.backupId = backupId; + this.signature = sign; + this.replacementId = jwk.kid; + this.replacementKey = jwk; + } + + export(): ExportContainer { + return new ExportContainer(this.backupId, JSON.stringify(this)); + } + static import(kx: ExportContainer): Delegation { + return JSON.parse(kx.payload); } } -class RecoveryMessage { +class RecoveryMessage { // ToDo Clean up backupCredId: string; delegationSignature: Uint8Array; pubKey: Uint8Array; @@ -77,18 +135,21 @@ class RecoveryMessage { // Dummy } + // ToDo Irgendwie ist der PubKey der in der RP registriert wird, nicht der PuBkey der im Plugin genutzt wird async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin: string, challenge: ArrayBuffer) { this.backupCredId = delegation.backupId; this.delegationSignature = base64ToByteArray(delegation.signature, true); // Create attestation object for new key - const recoveryCredId = base64ToByteArray(delegation.replacementId); + const recoveryCredId = base64ToByteArray(delegation.replacementId, true); // ToDo Irgenwie wird jetzt true gebraucht, obwohl vorher doch auch Base64 war? --> Das macht vlt attestation kapput? // ToDo New Credential should also contain recovery key + log.debug('init: delegation.replacementId', delegation.replacementId); const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); log.debug('AuthData of recovery message', authData); const coseKey = await rkPub.toCOSE(rkPub.publicKey); + log.debug('init: coseKey', rkPub.publicKey); this.pubKey = new Uint8Array(CBOR.encode(coseKey)); this.attestationObject = CBOR.encodeCanonical({ @@ -99,7 +160,7 @@ class RecoveryMessage { const clientData = await rkPub.generateClientData( - challenge, + challenge, { origin, type: 'webauthn.create' }, ); this.clientDataJSON = base64ToByteArray(window.btoa(clientData), true); @@ -119,19 +180,6 @@ class RecoveryMessage { } } -class Delegation { - signature: string; - backupId: string; - replacementId: string; - replacementKey: JsonWebKey; - constructor(sign, backupId, jwk) { - this.backupId = backupId; - this.signature = sign; - this.replacementId = jwk.kid; - this.replacementKey = jwk; - } -} - async function parseJWK(jwk, usages): Promise { return window.crypto.subtle.importKey( "jwk", @@ -145,97 +193,16 @@ async function parseJWK(jwk, usages): Promise { ); } - - -async function storePSKKeys(identifier: string, psk: Array): Promise { - let exportKeys = new Array(); - let i; - for (i = 0; i < psk.length; ++i) { - let parsedKey = await window.crypto.subtle.exportKey("jwk", psk[i].key); - exportKeys.push(new ExportKey(parsedKey, psk[i].id)); - } - let pskJSON = JSON.stringify(exportKeys); - - log.debug(`Storing ${identifier} keys`, pskJSON); - - return new Promise(async (res, rej) => { - chrome.storage.sync.set({ [identifier]: pskJSON }, () => { - if (!!chrome.runtime.lastError) { - log.warn(`Could not store ${identifier} keys`, pskJSON); - rej(chrome.runtime.lastError); - } else { - res(); - } - }); - }); -} - -async function storeDelegations(del: Array): Promise { - let delJSON = JSON.stringify(del); - - log.debug(`Storing ${DELEGATION}`, delJSON); - - return new Promise(async (res, rej) => { - chrome.storage.sync.set({ [DELEGATION]: delJSON }, () => { - if (!!chrome.runtime.lastError) { - log.warn(`Could not store ${DELEGATION}`, del); - rej(chrome.runtime.lastError); - } else { - res(); - } - }); - }); -} - -async function fetchPSKKeys(identifier: string, usages): Promise> { - return new Promise>(async (res, rej) => { - chrome.storage.sync.get(identifier, async (resp) => { - if (!!chrome.runtime.lastError) { - log.warn(`Could not fetch ${identifier} keys`); - rej(chrome.runtime.lastError); - return; - } - - let exportedKey = await JSON.parse(resp[identifier]); - let pskKeys = new Array(); - let i; - for (i = 0; i < exportedKey.length; ++i) { - let parsedKey = await parseJWK(exportedKey[i].key, usages); - pskKeys.push(new PSKKey(parsedKey, exportedKey[i].id)); - } - res(pskKeys); - }); - }); -} - -async function fetchDelegations(): Promise> { - return new Promise>(async (res, rej) => { - chrome.storage.sync.get(DELEGATION, async (resp) => { - if (!!chrome.runtime.lastError) { - log.warn(`Could not fetch ${DELEGATION}`); - rej(chrome.runtime.lastError); - return; - } - - let delegations = await JSON.parse(resp[DELEGATION]); - res(delegations); - }); - }); -} - -async function popPSKKey(identifier: string, usages): Promise { - let pskKeys = await fetchPSKKeys(identifier, usages); - if (pskKeys.length == 0) { - throw new Error(`No ${identifier} key available to pop`); +export async function getBackupKey(): Promise { + const container = await fetchExportContainer(BACKUP); + if (container.length == 0) { + throw new Error(`No backup key available`); } - let key = pskKeys.pop(); - await storePSKKeys(identifier, pskKeys) - log.info(`${pskKeys.length} ${identifier} keys left`); - return key; -} + const key = container.pop(); + await saveExportContainer(BACKUP, container); + log.debug(`${container.length} backup keys left`); -export async function popBackupKey(): Promise { - return popPSKKey(BACKUP, []); + return await BackupKey.import(key); } export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { @@ -255,24 +222,26 @@ async function pskRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise(); - let jwk = new Array(); + const jwk = new Array(); + const container = new Array(); let i; for (i = 0; i < n; ++i) { - let keyPair = await window.crypto.subtle.generateKey( + const keyPair = await window.crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: "P-256" }, true, ['sign'], ); - let rk = new RecoveryKey(keyPair.privateKey) - let expKey: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); - expKey.kid = rk.id; - - rcvKeys.push(rk); - jwk.push(expKey); + const bckKey = await getBackupKey(); + const rk = new RecoveryKey(keyPair.privateKey, bckKey); + const exportRk = await rk.export(); + const keyJWK: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); + keyJWK.kid = rk.id; + + container.push(exportRk); + jwk.push(keyJWK); } - await storePSKKeys(RECOVERY, rcvKeys); + await saveExportContainer(RECOVERY, container); // Download recovery public keys as file let json = [JSON.stringify(jwk)]; @@ -289,28 +258,17 @@ export async function createRecoveryKeys(n: number) { } async function getDelegation(credentialId: string): Promise { - const del = await fetchDelegations(); - log.debug('Fetched delegations', del); - const rec = del.filter(x => x.backupId === credentialId); - return rec.length != 0 ? rec[0] : null; + const container = await fetchExportContainer(DELEGATION); + log.debug('Fetched delegations', container); + const del = container.filter(x => x.id === credentialId); + return del.length != 0 ? (Delegation.import(del[0])) : null; } async function getRecoveryKey(credentialId: string): Promise { - const rks = await fetchPSKKeys(RECOVERY, ['sign']); - log.debug(rks); - const rk = rks.filter(x => x.id === credentialId); - return rk.length != 0 ? rk[0] : null; -} - -function createId(): string{ - let enc = new TextEncoder(); - let dt = new Date().getTime(); - const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - const r = (dt + Math.random()*16)%16 | 0; - dt = Math.floor(dt/16); - return (c=='x' ? r :(r&0x3|0x8)).toString(16); - }); - return byteArrayToBase64(enc.encode(uuid), true); + const container = await fetchExportContainer(RECOVERY); + log.debug(container); + const rk = container.filter(x => x.id === credentialId); + return rk.length != 0 ? (await RecoveryKey.import(rk[0])) : null; } class RecoveryOptions { @@ -343,9 +301,6 @@ export const recover = async ( return null; } - //origin = 'http://localhost:9005'; // Given origin does not work! - log.debug('origin', origin) - // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; const backupCredId: ArrayBuffer = requestedCredential.id as ArrayBuffer; @@ -355,18 +310,19 @@ export const recover = async ( const recOps = await getRecoveryOptions(encBackupCredId); log.debug('Recovery options', recOps); + const credId = base64ToByteArray(recOps.recoveryKey.id, true); + const encCredId = byteArrayToBase64(credId, true); + const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.recoveryKey.key); - log.debug('prv', rkPrv.privateKey); const rkPubRaw = await parseJWK(recOps.delegation.replacementKey, []); const rkPub = await getCompatibleKeyFromCryptoKey(rkPubRaw); - log.debug('pub', rkPub.publicKey); const recMessage = new RecoveryMessage(); await recMessage.init(recOps.delegation, rkPub, origin, publicKeyRequestOptions.challenge as ArrayBuffer); log.debug('Recovery message', recMessage); const extOutput = await pskRecoveryExtensionOutput(recMessage); - await saveKey(recOps.recoveryKey.id, rkPrv.privateKey, pin); + await saveKey(encCredId, rkPrv.privateKey, pin); const clientData = await rkPrv.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, @@ -390,9 +346,11 @@ export const recover = async ( concatData.set(clientDataHash, authenticatorData.length); const signature = await rkPrv.sign(concatData); + log.debug(clientData); + return { // ToDo Make getClientExtensionResults work - id: recOps.recoveryKey.id, - rawId: base64ToByteArray(recOps.recoveryKey.id, true), + id: encCredId, + rawId: credId, response: { authenticatorData: authenticatorData.buffer, clientDataJSON: clientDataJSON, diff --git a/src/storage.ts b/src/storage.ts index 9e9cf8e..463a88e 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,14 +1,17 @@ -import { ivLength, keyExportFormat, saltLength } from './constants'; -import { base64ToByteArray, byteArrayToBase64, concatenate } from './utils'; +import {ivLength, keyExportFormat, saltLength} from './constants'; +import {base64ToByteArray, byteArrayToBase64, concatenate} from './utils'; import {getLogger} from "./logging"; +import {ExportContainer, ExportContainerType} from "./recovery"; + +const log = getLogger('storage'); export const keyExists = (key: string): Promise => { return new Promise(async (res, rej) => { - chrome.storage.sync.get(key, (resp) => { + chrome.storage.sync.get({[key]: null}, (resp) => { if (!!chrome.runtime.lastError) { rej(chrome.runtime.lastError); } else { - res(!!resp[key]); + res(!(resp[key] == null)); } }); }); @@ -26,7 +29,7 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise const derivationKey = await window.crypto.subtle.importKey( 'raw', enc.encode(pin), - { name: 'PBKDF2', length: 256 }, + {name: 'PBKDF2', length: 256}, false, ['deriveBits', 'deriveKey'], ); @@ -39,22 +42,63 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise return window.crypto.subtle.deriveKey( pbkdf2Params, derivationKey, - { name: 'AES-GCM', length: 256 }, + {name: 'AES-GCM', length: 256}, true, ['wrapKey', 'unwrapKey'], ); }; -const log = getLogger('storage'); +export async function saveExportContainer(cType: ExportContainerType, container: Array): Promise { + let exportJSON = JSON.stringify(container); + + log.debug(`Storing ${cType} container`, exportJSON); + + return new Promise(async (res, rej) => { + chrome.storage.sync.set({[cType]: exportJSON}, () => { + if (!!chrome.runtime.lastError) { + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); +} + +export async function fetchExportContainer(cType: ExportContainerType): Promise> { + return new Promise>(async (res, rej) => { + chrome.storage.sync.get({[cType]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.warn(`Could not fetch ${cType} container`); + rej(chrome.runtime.lastError); + return; + } + + if (resp[cType] == null) { + return rej(`Container ${cType} not found`); + } + + let exportJSON = await JSON.parse(resp[cType]); + let exportContainer = new Array(); + let i; + for (i = 0; i < exportJSON.length; ++i) { + exportContainer.push(new ExportContainer(exportJSON[i].id, exportJSON[i].payload)); + } + res(exportContainer); + }); + }); +} export const fetchKey = async (key: string, pin: string): Promise => { log.debug('Fetching key for', key); return new Promise(async (res, rej) => { - chrome.storage.sync.get(key, async (resp) => { + chrome.storage.sync.get({[key]: null}, async (resp) => { if (!!chrome.runtime.lastError) { rej(chrome.runtime.lastError); return; } + if (resp[key] == null) { + return rej("Key not found"); + } const payload = base64ToByteArray(resp[key]); const saltByteLength = payload[0]; const ivByteLength = payload[1]; @@ -116,12 +160,10 @@ export const saveKey = (key: string, privateKey: CryptoKey, pin: string): Promis keyAlgorithm, wrappedKey); - chrome.storage.sync.set({ [key]: byteArrayToBase64(payload) }, () => { + chrome.storage.sync.set({[key]: byteArrayToBase64(payload)}, () => { if (!!chrome.runtime.lastError) { - log.info("Key not stored") rej(chrome.runtime.lastError); } else { - log.info("Key stored") res(); } }); diff --git a/src/webauthn.ts b/src/webauthn.ts index f792578..5151e3d 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -4,17 +4,15 @@ import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; import { - createRecoveryKeys, - popBackupKey, + getBackupKey, PSK, pskSetupExtensionOutput, recover, - syncBackupKeys, - syncDelegation } from "./recovery"; const log = getLogger('webauthn'); -export const generateRegistrationKeyAndAttestation = async ( +// Attestation +export const processCredentialCreation = async ( origin: string, publicKeyCreationOptions: PublicKeyCredentialCreationOptions, pin: string, @@ -23,34 +21,39 @@ export const generateRegistrationKeyAndAttestation = async ( log.warn('We can perform only none attestation'); return null; } - log.info(JSON.stringify(publicKeyCreationOptions.extensions)); - // ToDo Trigger PSK flow only if RP signals extension support + + let supportRecovery = false; + const reqExt: any = publicKeyCreationOptions.extensions; + if (reqExt !== undefined) { + if (reqExt.hasOwnProperty(PSK)) { + supportRecovery = true; + log.info('RP supports PSK'); + } + } const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - // await syncBackupKeys(); - // await createRecoveryKeys(5); - // await syncDelegation(); - // return; - - let bckpKey = await popBackupKey(); - log.info('Used backup key', bckpKey); - - const pskExt = await pskSetupExtensionOutput(bckpKey); + let bckpKey = await getBackupKey(); + log.info('Use backup key', bckpKey); - const credentialId = base64ToByteArray(bckpKey.id, true); - const encCredId = byteArrayToBase64(credentialId, true); + const credId = base64ToByteArray(bckpKey.id, true); + const encCredId = byteArrayToBase64(credId, true); - // Check if there is already a key for this rp ID if (await keyExists(encCredId)) { - throw new Error(`key with id ${encCredId} already exists`); + throw new Error(`credential with id ${encCredId} already exists`); } let compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); - // TODO Increase key counter - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credentialId, pskExt); + let extOutput = null; + if (supportRecovery) { + extOutput = await pskSetupExtensionOutput(bckpKey); + } + const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId, extOutput); + + // ToDo Add support for credential counter + const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, @@ -62,15 +65,14 @@ export const generateRegistrationKeyAndAttestation = async ( fmt: 'none', }).buffer; - // Now that we have built all we need, let's save the key await saveKey(encCredId, compatibleKey.privateKey, pin); - log.debug('send attestation'); + log.debug('Attestation created'); return { getClientExtensionResults: () => ({}), // ToDo Put PSK extension data id: encCredId, - rawId: credentialId, + rawId: credId, response: { attestationObject, clientDataJSON: base64ToByteArray(window.btoa(clientData)), @@ -80,36 +82,33 @@ export const generateRegistrationKeyAndAttestation = async ( }; // Assertion -export const generateKeyRequestAndAssertion = async ( +export const processCredentialRequest = async ( origin: string, publicKeyRequestOptions: PublicKeyCredentialRequestOptions, pin: string, ): Promise => { if (!publicKeyRequestOptions.allowCredentials) { - log.debug('No keys requested'); + log.debug('No credentials requested'); return null; } - // origin = 'http://localhost:9005'; // Given origin does not work! - log.debug('origin', origin) - - log.debug(JSON.stringify(publicKeyRequestOptions.extensions)); const reqExt: any = publicKeyRequestOptions.extensions; if (reqExt !== undefined) { if (reqExt.hasOwnProperty(PSK)) { + log.debug('Recovery requested'); return await recover(origin, publicKeyRequestOptions, pin); } } - // For now we will only worry about the first entry - const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; - const credentialId: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const encCredId = byteArrayToBase64(new Uint8Array(credentialId), true); + const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; // ToDo Handle all entries + const credId: ArrayBuffer = requestedCredential.id as ArrayBuffer; + const encCredId = byteArrayToBase64(new Uint8Array(credId), true); + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); const key = await fetchKey(encCredId, pin); if (!key) { - throw new Error(`key with id ${encCredId} not found`); + throw new Error(`credential with id ${encCredId} not found`); } const compatibleKey = await getCompatibleKeyFromCryptoKey(key); const clientData = await compatibleKey.generateClientData( @@ -125,23 +124,26 @@ export const generateKeyRequestAndAssertion = async ( const clientDataJSON = base64ToByteArray(window.btoa(clientData)); const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + // ToDo Update counter const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); + // Prepare input for signature const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); concatData.set(clientDataHash, authenticatorData.length); const signature = await compatibleKey.sign(concatData); - log.info('signature', signature); + log.debug('signature', signature); + log.debug('clientData', clientData); + return { id: encCredId, - rawId: credentialId, + rawId: credId, response: { authenticatorData: authenticatorData.buffer, clientDataJSON: clientDataJSON, signature: (new Uint8Array(signature)).buffer, - userHandle: new ArrayBuffer(0), // This should be nullable + userHandle: new ArrayBuffer(0), }, type: 'public-key', } as Credential; From d06fb4cdeb8ffcfb6887f847169e28ffc73b0f2e Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 20 Jul 2020 18:39:33 +0200 Subject: [PATCH 20/81] Clean up --- src/background.ts | 10 +-- src/recovery.ts | 221 ++++++++++++++++++++++------------------------ src/webauthn.ts | 8 +- 3 files changed, 116 insertions(+), 123 deletions(-) diff --git a/src/background.ts b/src/background.ts index a649c7e..b23f4b7 100644 --- a/src/background.ts +++ b/src/background.ts @@ -2,7 +2,7 @@ import {disabledIcons, enabledIcons} from './constants'; import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {processCredentialRequest, processCredentialCreation} from './webauthn'; -import {createRecoveryKeys, syncBackupKeys, syncDelegation} from "./recovery"; +import {RecoveryKey, syncBackupKeys, syncDelegation} from "./recovery"; const log = getLogger('background'); @@ -35,21 +35,21 @@ const requestPin = async (tabId: number, origin: string, newPin: boolean = true) }; const syncBackup = async (backupContent) => { - console.log('Sync Backup called'); + log.debug('Sync Backup called'); await syncBackupKeys(backupContent); }; const syncDel = async (delegationContent) => { - console.log('Sync Delegation called'); + log.debug('Sync Delegation called'); await syncDelegation(delegationContent); }; const recovery = async (n) => { - console.log('Create recovery keys called') + log.debug('Create recovery keys called') - await createRecoveryKeys(n); + await RecoveryKey.generate(n); } const create = async (msg, sender: chrome.runtime.MessageSender) => { diff --git a/src/recovery.ts b/src/recovery.ts index 717fc8c..678108b 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -35,10 +35,10 @@ export async function syncDelegation (content) { let i; const container = new Array(); for (i = 0; i < rawDelegations.length; ++i) { - const sign = rawDelegations[i].signature; - const bId = base64ToByteArray(rawDelegations[i].cred_id, true); - const encBId = byteArrayToBase64(bId, true); - const del = new Delegation(sign, encBId, rawDelegations[i].public_key); + const sign = rawDelegations[i].sign; + const srcCredId = base64ToByteArray(rawDelegations[i].src_cred_id, true); + const encSrcCredId = byteArrayToBase64(srcCredId, true); + const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pub_rk); container.push(del.export()); } log.debug("Loaded delegation", container); @@ -74,6 +74,18 @@ export class BackupKey { const key = await parseJWK(rawKey, []); return new BackupKey(key, kx.id); } + + static async get(): Promise { + const container = await fetchExportContainer(BACKUP); + if (container.length == 0) { + throw new Error(`No backup key available`); + } + const key = container.pop(); + await saveExportContainer(BACKUP, container); + log.debug(`${container.length} backup keys left`); + + return await BackupKey.import(key); + } } export class RecoveryKey { @@ -102,32 +114,74 @@ export class RecoveryKey { return new RecoveryKey(key, backupKey); } + + static async generate(n: number) { + const jwk = new Array(); + const container = new Array(); + let i; + for (i = 0; i < n; ++i) { + const keyPair = await window.crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: "P-256" }, + true, + ['sign'], + ); + const bckKey = await BackupKey.get(); + const rk = new RecoveryKey(keyPair.privateKey, bckKey); + const exportRk = await rk.export(); + const pubJWK: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); + pubJWK.kid = rk.id; + + container.push(exportRk); + jwk.push(pubJWK); + } + + await saveExportContainer(RECOVERY, container); + + // Download recovery public keys as file + let json = [JSON.stringify(jwk)]; + let blob1 = new Blob(json, { type: "text/plain;charset=utf-8" }); + let link = (window.URL ? URL : webkitURL).createObjectURL(blob1); + let a = document.createElement("a"); + a.download = "recoveryKeys.json"; + a.href = link; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + log.debug("Downloading recovery keys completed"); + } } class Delegation { - signature: string; - backupId: string; - replacementId: string; - replacementKey: JsonWebKey; - constructor(sign, backupId, jwk) { - this.backupId = backupId; - this.signature = sign; - this.replacementId = jwk.kid; - this.replacementKey = jwk; + sign: string; + srcCredId: string; + rkId: string; + pubRK: JsonWebKey; + constructor(sign, srcCredId, jwk) { + this.srcCredId = srcCredId; + this.sign = sign; + this.rkId = jwk.kid; + this.pubRK = jwk; } export(): ExportContainer { - return new ExportContainer(this.backupId, JSON.stringify(this)); + return new ExportContainer(this.srcCredId, JSON.stringify(this)); } static import(kx: ExportContainer): Delegation { return JSON.parse(kx.payload); } + + static async getById(srcCredId: string): Promise { + const container = await fetchExportContainer(DELEGATION); + log.debug('Fetched delegations', container); + const del = container.filter(x => x.id === srcCredId); + return del.length != 0 ? (Delegation.import(del[0])) : null; + } } -class RecoveryMessage { // ToDo Clean up - backupCredId: string; - delegationSignature: Uint8Array; - pubKey: Uint8Array; +class RecoveryMessage { + srcCredId: string; + delSign: Uint8Array; + pubRK: Uint8Array; attestationObject: Uint8Array; clientDataJSON: Uint8Array; @@ -135,22 +189,18 @@ class RecoveryMessage { // ToDo Clean up // Dummy } - // ToDo Irgendwie ist der PubKey der in der RP registriert wird, nicht der PuBkey der im Plugin genutzt wird async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin: string, challenge: ArrayBuffer) { - this.backupCredId = delegation.backupId; - this.delegationSignature = base64ToByteArray(delegation.signature, true); + this.srcCredId = delegation.srcCredId; + this.delSign = base64ToByteArray(delegation.sign, true); // Create attestation object for new key - const recoveryCredId = base64ToByteArray(delegation.replacementId, true); // ToDo Irgenwie wird jetzt true gebraucht, obwohl vorher doch auch Base64 war? --> Das macht vlt attestation kapput? + const recoveryCredId = base64ToByteArray(delegation.rkId, true); - // ToDo New Credential should also contain recovery key - log.debug('init: delegation.replacementId', delegation.replacementId); + // ToDo New Credential should also contain backup key const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); - log.debug('AuthData of recovery message', authData); const coseKey = await rkPub.toCOSE(rkPub.publicKey); - log.debug('init: coseKey', rkPub.publicKey); - this.pubKey = new Uint8Array(CBOR.encode(coseKey)); + this.pubRK = new Uint8Array(CBOR.encode(coseKey)); this.attestationObject = CBOR.encodeCanonical({ attStmt: new Map(), @@ -169,9 +219,8 @@ class RecoveryMessage { // ToDo Clean up encode(): ArrayBuffer { return CBOR.encodeCanonical({ - publicKey: this.pubKey, - delegationSignature: byteArrayToBase64(this.delegationSignature), - backupCredentialId: this.backupCredId, + delSign: byteArrayToBase64(this.delSign, true), + srcCredId: this.srcCredId, authAttData: { clientDataJSON: this.clientDataJSON, attestationObject: this.attestationObject @@ -193,98 +242,43 @@ async function parseJWK(jwk, usages): Promise { ); } -export async function getBackupKey(): Promise { - const container = await fetchExportContainer(BACKUP); - if (container.length == 0) { - throw new Error(`No backup key available`); - } - const key = container.pop(); - await saveExportContainer(BACKUP, container); - log.debug(`${container.length} backup keys left`); - - return await BackupKey.import(key); -} - -export async function pskSetupExtensionOutput(backupKey: BackupKey): Promise { +export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); const coseKey = await compatibleKey.toCOSE(backupKey.key); let encodedKey = new Uint8Array(CBOR.encode(coseKey)); - log.debug(encodedKey); - let extOutput = new Map([[PSK, encodedKey]]); return new Uint8Array(CBOR.encode(extOutput)); } -async function pskRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { +async function createPSKRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { let extOutput = new Map([[PSK, recMsg.encode()]]); return new Uint8Array(CBOR.encode(extOutput)); } -export async function createRecoveryKeys(n: number) { - const jwk = new Array(); - const container = new Array(); - let i; - for (i = 0; i < n; ++i) { - const keyPair = await window.crypto.subtle.generateKey( - { name: 'ECDSA', namedCurve: "P-256" }, - true, - ['sign'], - ); - const bckKey = await getBackupKey(); - const rk = new RecoveryKey(keyPair.privateKey, bckKey); - const exportRk = await rk.export(); - const keyJWK: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); - keyJWK.kid = rk.id; - - container.push(exportRk); - jwk.push(keyJWK); - } - await saveExportContainer(RECOVERY, container); - - // Download recovery public keys as file - let json = [JSON.stringify(jwk)]; - let blob1 = new Blob(json, { type: "text/plain;charset=utf-8" }); - let link = (window.URL ? URL : webkitURL).createObjectURL(blob1); - let a = document.createElement("a"); - a.download = "recoveryKeys.json"; - a.href = link; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - log.debug("Downloading recovery keys completed"); - -} - -async function getDelegation(credentialId: string): Promise { - const container = await fetchExportContainer(DELEGATION); - log.debug('Fetched delegations', container); - const del = container.filter(x => x.id === credentialId); - return del.length != 0 ? (Delegation.import(del[0])) : null; -} -async function getRecoveryKey(credentialId: string): Promise { +async function getRecoveryKey(id: string): Promise { const container = await fetchExportContainer(RECOVERY); log.debug(container); - const rk = container.filter(x => x.id === credentialId); + const rk = container.filter(x => x.id === id); return rk.length != 0 ? (await RecoveryKey.import(rk[0])) : null; } class RecoveryOptions { - recoveryKey: RecoveryKey; - delegation: Delegation; + rk: RecoveryKey; + del: Delegation; constructor(rk: RecoveryKey, del: Delegation) { - this.delegation = del; - this.recoveryKey = rk; + this.del = del; + this.rk = rk; } } -async function getRecoveryOptions(backupCredentialId: string): Promise { - const del = await getDelegation(backupCredentialId); +async function getRecoveryOptions(srcCredId: string): Promise { + const del = await Delegation.getById(srcCredId); log.debug('Use delegation', del); - const rk = await getRecoveryKey(del.replacementId); + const rk = await getRecoveryKey(del.rkId); log.debug('Use recovery key', rk); return new RecoveryOptions(rk, del); } @@ -303,28 +297,28 @@ export const recover = async ( // For now we will only worry about the first entry const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; - const backupCredId: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const encBackupCredId = byteArrayToBase64(new Uint8Array(backupCredId), true); - log.info('Started recovery for', encBackupCredId); + const srcCredId: ArrayBuffer = requestedCredential.id as ArrayBuffer; + const encSrcCredId = byteArrayToBase64(new Uint8Array(srcCredId), true); + log.info('Started recovery for', encSrcCredId); - const recOps = await getRecoveryOptions(encBackupCredId); + const recOps = await getRecoveryOptions(encSrcCredId); log.debug('Recovery options', recOps); - const credId = base64ToByteArray(recOps.recoveryKey.id, true); - const encCredId = byteArrayToBase64(credId, true); + const rkId = base64ToByteArray(recOps.rk.id, true); + const encRkId = byteArrayToBase64(rkId, true); - const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.recoveryKey.key); - const rkPubRaw = await parseJWK(recOps.delegation.replacementKey, []); - const rkPub = await getCompatibleKeyFromCryptoKey(rkPubRaw); + const prvRK = await getCompatibleKeyFromCryptoKey(recOps.rk.key); + const rawPubRK = await parseJWK(recOps.del.pubRK, []); + const pubRK = await getCompatibleKeyFromCryptoKey(rawPubRK); const recMessage = new RecoveryMessage(); - await recMessage.init(recOps.delegation, rkPub, origin, publicKeyRequestOptions.challenge as ArrayBuffer); + await recMessage.init(recOps.del, pubRK, origin, publicKeyRequestOptions.challenge as ArrayBuffer); log.debug('Recovery message', recMessage); - const extOutput = await pskRecoveryExtensionOutput(recMessage); + const extOutput = await createPSKRecoveryExtensionOutput(recMessage); - await saveKey(encCredId, rkPrv.privateKey, pin); + await saveKey(encRkId, prvRK.privateKey, pin); - const clientData = await rkPrv.generateClientData( + const clientData = await prvRK.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, { origin, @@ -339,18 +333,17 @@ export const recover = async ( const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); + const authenticatorData = await prvRK.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); concatData.set(clientDataHash, authenticatorData.length); - const signature = await rkPrv.sign(concatData); - log.debug(clientData); + const signature = await prvRK.sign(concatData); return { // ToDo Make getClientExtensionResults work - id: encCredId, - rawId: credId, + id: encRkId, + rawId: rkId, response: { authenticatorData: authenticatorData.buffer, clientDataJSON: clientDataJSON, diff --git a/src/webauthn.ts b/src/webauthn.ts index 5151e3d..8198374 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -4,9 +4,9 @@ import { getLogger } from './logging'; import { fetchKey, keyExists, saveKey } from './storage'; import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; import { - getBackupKey, + BackupKey, PSK, - pskSetupExtensionOutput, recover, + createPSKSetupExtensionOutput, recover, } from "./recovery"; const log = getLogger('webauthn'); @@ -34,7 +34,7 @@ export const processCredentialCreation = async ( const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - let bckpKey = await getBackupKey(); + let bckpKey = await BackupKey.get(); log.info('Use backup key', bckpKey); const credId = base64ToByteArray(bckpKey.id, true); @@ -48,7 +48,7 @@ export const processCredentialCreation = async ( let extOutput = null; if (supportRecovery) { - extOutput = await pskSetupExtensionOutput(bckpKey); + extOutput = await createPSKSetupExtensionOutput(bckpKey); } const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId, extOutput); From 02dade47518903ba117607331113ac85bd92b09d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 24 Jul 2020 17:55:43 +0200 Subject: [PATCH 21/81] Add new backup key during recovery --- src/recovery.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 678108b..322903f 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -189,15 +189,15 @@ class RecoveryMessage { // Dummy } - async init(delegation: Delegation, rkPub: ICOSECompatibleKey, origin: string, challenge: ArrayBuffer) { + async init(delegation: Delegation, rkPub: ICOSECompatibleKey, backupKey: BackupKey, origin: string, challenge: ArrayBuffer) { this.srcCredId = delegation.srcCredId; this.delSign = base64ToByteArray(delegation.sign, true); // Create attestation object for new key const recoveryCredId = base64ToByteArray(delegation.rkId, true); - // ToDo New Credential should also contain backup key - const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, null); + const pskExtOutput = await createPSKSetupExtensionOutput(backupKey); + const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, pskExtOutput); const coseKey = await rkPub.toCOSE(rkPub.publicKey); this.pubRK = new Uint8Array(CBOR.encode(coseKey)); @@ -307,18 +307,18 @@ export const recover = async ( const rkId = base64ToByteArray(recOps.rk.id, true); const encRkId = byteArrayToBase64(rkId, true); - const prvRK = await getCompatibleKeyFromCryptoKey(recOps.rk.key); - const rawPubRK = await parseJWK(recOps.del.pubRK, []); - const pubRK = await getCompatibleKeyFromCryptoKey(rawPubRK); + const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.rk.key); + const rawRKPub = await parseJWK(recOps.del.pubRK, []); + const rkPub = await getCompatibleKeyFromCryptoKey(rawRKPub); const recMessage = new RecoveryMessage(); - await recMessage.init(recOps.del, pubRK, origin, publicKeyRequestOptions.challenge as ArrayBuffer); + await recMessage.init(recOps.del, rkPub, recOps.rk.backupKey, origin, publicKeyRequestOptions.challenge as ArrayBuffer); log.debug('Recovery message', recMessage); const extOutput = await createPSKRecoveryExtensionOutput(recMessage); - await saveKey(encRkId, prvRK.privateKey, pin); + await saveKey(encRkId, rkPrv.privateKey, pin); - const clientData = await prvRK.generateClientData( + const clientData = await rkPrv.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, { origin, @@ -333,13 +333,13 @@ export const recover = async ( const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await prvRK.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); + const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); concatData.set(clientDataHash, authenticatorData.length); - const signature = await prvRK.sign(concatData); + const signature = await rkPrv.sign(concatData); return { // ToDo Make getClientExtensionResults work id: encRkId, From 033d3371bce5037ba2f1d0ac19c480e0bd2e18a9 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 25 Jul 2020 13:49:44 +0200 Subject: [PATCH 22/81] Use CBOR canonical encoding --- src/recovery.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 322903f..a8a66b3 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -200,7 +200,7 @@ class RecoveryMessage { const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, pskExtOutput); const coseKey = await rkPub.toCOSE(rkPub.publicKey); - this.pubRK = new Uint8Array(CBOR.encode(coseKey)); + this.pubRK = new Uint8Array(CBOR.encodeCanonical(coseKey)); this.attestationObject = CBOR.encodeCanonical({ attStmt: new Map(), @@ -245,15 +245,15 @@ async function parseJWK(jwk, usages): Promise { export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); const coseKey = await compatibleKey.toCOSE(backupKey.key); - let encodedKey = new Uint8Array(CBOR.encode(coseKey)); + let encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); let extOutput = new Map([[PSK, encodedKey]]); - return new Uint8Array(CBOR.encode(extOutput)); + return new Uint8Array(CBOR.encodeCanonical(extOutput)); } async function createPSKRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { let extOutput = new Map([[PSK, recMsg.encode()]]); - return new Uint8Array(CBOR.encode(extOutput)); + return new Uint8Array(CBOR.encodeCanonical(extOutput)); } From 5903ceac1ba27db025dea299a12c1b29ce97d01d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 25 Jul 2020 14:50:29 +0200 Subject: [PATCH 23/81] Fix encoding for key inside authenticator data --- src/crypto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto.ts b/src/crypto.ts index f1fd7cb..85010f5 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -108,7 +108,7 @@ class ECDSA implements ICOSECompatibleKey { credIdLen[0] = (credentialId.length >> 8) & 0xff; credIdLen[1] = credentialId.length & 0xff; const coseKey = await this.toCOSE(this.publicKey); - encodedKey = new Uint8Array(CBOR.encode(coseKey)); + encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); authenticatorDataLength += aaguid.length + credIdLen.length + credentialId.length From 51a9bc0728b56078cc29f604cbfb91e33fe43e45 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 26 Jul 2020 14:53:13 +0200 Subject: [PATCH 24/81] Use REST endpoint for backup device communication --- dist/chromium/popup.html | 11 +---- package-lock.json | 29 ++++++++++- package.json | 1 + src/background.ts | 32 +++++------- src/popup.ts | 48 ++---------------- src/recovery.ts | 104 ++++++++++++++++++++++++--------------- src/storage.ts | 1 + 7 files changed, 111 insertions(+), 115 deletions(-) diff --git a/dist/chromium/popup.html b/dist/chromium/popup.html index adadc38..f371d9b 100644 --- a/dist/chromium/popup.html +++ b/dist/chromium/popup.html @@ -19,15 +19,8 @@
PSK Options -
- Backup File Sync: -
-
- Delegation File Sync: -
-
- -
+ +
diff --git a/package-lock.json b/package-lock.json index de08684..0ae85f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1341,6 +1341,14 @@ "integrity": "sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==", "dev": true }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -3805,6 +3813,24 @@ "readable-stream": "^2.3.6" } }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -6862,8 +6888,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "nan": { "version": "2.14.0", diff --git a/package.json b/package.json index 2ed46ac..d3e6f38 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/loglevel": "^1.6.3", "@types/webappsec-credential-management": "^0.3.11", "asn1js": "^2.0.26", + "axios": "^0.19.2", "bn.js": "^5.1.2", "cbor": "^4.3.0", "elliptic": "^6.5.3", diff --git a/src/background.ts b/src/background.ts index b23f4b7..a961642 100644 --- a/src/background.ts +++ b/src/background.ts @@ -2,7 +2,8 @@ import {disabledIcons, enabledIcons} from './constants'; import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {processCredentialRequest, processCredentialCreation} from './webauthn'; -import {RecoveryKey, syncBackupKeys, syncDelegation} from "./recovery"; +import {BackupDeviceBaseUrl, RecoveryKey, pskSetup, pskRecovery} from "./recovery"; +import * as axios from 'axios'; const log = getLogger('background'); @@ -34,22 +35,14 @@ const requestPin = async (tabId: number, origin: string, newPin: boolean = true) return pin; }; -const syncBackup = async (backupContent) => { - log.debug('Sync Backup called'); - - await syncBackupKeys(backupContent); -}; - -const syncDel = async (delegationContent) => { - log.debug('Sync Delegation called'); - - await syncDelegation(delegationContent); +const setup = async () => { + log.debug('Setup called'); + await pskSetup(); }; -const recovery = async (n) => { - log.debug('Create recovery keys called') - - await RecoveryKey.generate(n); +const recovery = async () => { + log.debug('Recovery called!') + await pskRecovery(); } const create = async (msg, sender: chrome.runtime.MessageSender) => { @@ -122,14 +115,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { delete (pinProtectedCallbacks[msg.tabId]); } break; - case 'syncBackup': - syncBackup(msg.backup).then(() => alert("Backup file processed")); - break; - case 'syncDelegation': - syncDel(msg.delegation).then(() => alert("Delegation file processed")); + case 'setup': + setup().then(() => alert("Backup keys synchronized successfully!")); break; case 'recovery': - recovery(msg.amount).then(() => alert("Creating recovery keys finished")) + recovery().then(() => alert("Recovery finished successfully!")) break; default: sendResponse(null); diff --git a/src/popup.ts b/src/popup.ts index 7071ff3..082a631 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -11,54 +11,16 @@ $(() => { return; } - $('#delegationFile').on('change', function(evt: Event) { - const files = (evt.target).files; // FileList object - - // use the 1st file from the list - const f = files[0]; - - const reader = new FileReader(); - - // Closure to capture the file information. - reader.onload = (function(theFile) { - return function(e) { - chrome.runtime.sendMessage({ - delegation: e.target.result, - type: 'syncDelegation', - }); - }; - })(f); - - // Read in the image file as a data URL. - reader.readAsText(f); - }); - $('#backupFile').on('change', function(evt: Event) { + $('#Setup').on('click', function(evt: Event) { evt.preventDefault(); - const files = (evt.target).files; // FileList object - - // use the 1st file from the list - const f = files[0]; - - const reader = new FileReader(); - - // Closure to capture the file information. - reader.onload = (function(theFile) { - return function(e) { - chrome.runtime.sendMessage({ - backup: e.target.result, - type: 'syncBackup', - }); - }; - })(f); - - // Read in the image file as a data URL. - reader.readAsText(f); + chrome.runtime.sendMessage({ + type: 'setup', + }); }); - $('#recovery').on('click', function(evt: Event) { + $('#Recovery').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - amount: 5, // ToDo Read real input type: 'recovery', }); }); diff --git a/src/recovery.ts b/src/recovery.ts index a8a66b3..93321f5 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -3,46 +3,80 @@ import {getLogger} from './logging'; import {getCompatibleKeyFromCryptoKey, ICOSECompatibleKey} from './crypto'; import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; +import * as axios from "axios"; const log = getLogger('recovery'); export const PSK: string = 'psk' +export const BackupDeviceBaseUrl = 'http://localhost:8005' // ToDo Load from a config file + export type ExportContainerType = string const BACKUP: ExportContainerType = 'backup' const RECOVERY: ExportContainerType = 'recovery' const DELEGATION: ExportContainerType = 'delegation' -export async function syncBackupKeys (content) { - const jwk = JSON.parse(content); - let i; - const container = new Array(); - for (i = 0; i < jwk.length; ++i) { - const parsedKey = await parseJWK(jwk[i], []); - const id = base64ToByteArray(jwk[i].kid, true); - const encId = byteArrayToBase64(id, true); - const bckpKey = new BackupKey(parsedKey, encId); - const expBckpKey = await bckpKey.export(); - container.push(expBckpKey); - } - log.debug('Loaded backup keys', container); - - await saveExportContainer(BACKUP, container); +export async function pskSetup () { + const authId = prompt("Please enter a name for your authenticator", "MyAuth"); + const keyAmount: number = +prompt("How many backup keys should be created?", "5"); + + await axios.default.post(BackupDeviceBaseUrl + '/setup', {auth_id: authId, key_amount: keyAmount}) + .then(async function (response) { + log.debug(response) + const jwk = response.data; + let i; + const container = new Array(); + for (i = 0; i < jwk.length; ++i) { + const parsedKey = await parseJWK(jwk[i], []); + const id = base64ToByteArray(jwk[i].kid, true); + const encId = byteArrayToBase64(id, true); + const bckpKey = new BackupKey(parsedKey, encId); + const expBckpKey = await bckpKey.export(); + container.push(expBckpKey); + } + log.debug('Loaded backup keys', container); + + await saveExportContainer(BACKUP, container); + }) + .catch(function (error) { + log.error(error); + }) } -export async function syncDelegation (content) { - const rawDelegations = JSON.parse(content); - let i; - const container = new Array(); - for (i = 0; i < rawDelegations.length; ++i) { - const sign = rawDelegations[i].sign; - const srcCredId = base64ToByteArray(rawDelegations[i].src_cred_id, true); - const encSrcCredId = byteArrayToBase64(srcCredId, true); - const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pub_rk); - container.push(del.export()); - } - log.debug("Loaded delegation", container); - await saveExportContainer(DELEGATION, container); +export async function pskRecovery () { + const authId = prompt("Which authenticator you want to replace?", "MyAuth"); + + await axios.default.get(BackupDeviceBaseUrl + '/recovery?authId=' + authId) + .then(async function (response1) { + log.debug(response1); + const keyAmount = response1.data.key_amount; + + const rkPub = await RecoveryKey.generate(keyAmount); + + await axios.default.post(BackupDeviceBaseUrl + '/recovery', {recovery_keys: rkPub, auth_id: authId}) + .then(async function (response2) { + log.debug(response2); + const rawDelegations = response2.data; + + let i; + const container = new Array(); + for (i = 0; i < rawDelegations.length; ++i) { + const sign = rawDelegations[i].sign; + const srcCredId = base64ToByteArray(rawDelegations[i].src_cred_id, true); + const encSrcCredId = byteArrayToBase64(srcCredId, true); + const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pub_rk); + container.push(del.export()); + } + log.debug("Loaded delegation", container); + await saveExportContainer(DELEGATION, container); + }) + .catch(function (error) { + log.error(error); + }) + }) + .catch(function (error) { + log.error(error); + }) } export class ExportContainer { @@ -115,7 +149,7 @@ export class RecoveryKey { return new RecoveryKey(key, backupKey); } - static async generate(n: number) { + static async generate(n: number): Promise> { const jwk = new Array(); const container = new Array(); let i; @@ -137,17 +171,7 @@ export class RecoveryKey { await saveExportContainer(RECOVERY, container); - // Download recovery public keys as file - let json = [JSON.stringify(jwk)]; - let blob1 = new Blob(json, { type: "text/plain;charset=utf-8" }); - let link = (window.URL ? URL : webkitURL).createObjectURL(blob1); - let a = document.createElement("a"); - a.download = "recoveryKeys.json"; - a.href = link; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - log.debug("Downloading recovery keys completed"); + return jwk; } } diff --git a/src/storage.ts b/src/storage.ts index 463a88e..27f15c8 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -99,6 +99,7 @@ export const fetchKey = async (key: string, pin: string): Promise => if (resp[key] == null) { return rej("Key not found"); } + log.info('PIN', pin); const payload = base64ToByteArray(resp[key]); const saltByteLength = payload[0]; const ivByteLength = payload[1]; From 82a80fd3dbcf249a3058070a4a54671713c4d692 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Tue, 28 Jul 2020 19:28:13 +0200 Subject: [PATCH 25/81] Store attestation object inside backup key --- src/recovery.ts | 38 ++++++++++++++++++++------------------ src/webauthn.ts | 6 +++--- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 93321f5..a8a1078 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -23,14 +23,16 @@ export async function pskSetup () { await axios.default.post(BackupDeviceBaseUrl + '/setup', {auth_id: authId, key_amount: keyAmount}) .then(async function (response) { log.debug(response) - const jwk = response.data; + const stpRsp = response.data; let i; const container = new Array(); - for (i = 0; i < jwk.length; ++i) { - const parsedKey = await parseJWK(jwk[i], []); - const id = base64ToByteArray(jwk[i].kid, true); + for (i = 0; i < stpRsp.length; ++i) { + const jwk = stpRsp[i].jwk; + const attObj = stpRsp[i].att_obj; + const parsedKey = await parseJWK(jwk, []); + const id = base64ToByteArray(jwk.kid, true); const encId = byteArrayToBase64(id, true); - const bckpKey = new BackupKey(parsedKey, encId); + const bckpKey = new BackupKey(parsedKey, encId, attObj); const expBckpKey = await bckpKey.export(); container.push(expBckpKey); } @@ -91,22 +93,24 @@ export class ExportContainer { export class BackupKey { key: CryptoKey; + attObj: string; id: string; - constructor(key: CryptoKey, id: string) { + constructor(key: CryptoKey, id: string, attObj: string) { this.key = key; this.id = id; + this.attObj = attObj; } async export(): Promise { const jwk = await window.crypto.subtle.exportKey("jwk", this.key); - const encJWK = JSON.stringify(jwk); - return new ExportContainer(this.id, encJWK); + const rawJSON = {parsedKey: jwk, attObj: this.attObj}; + return new ExportContainer(this.id, JSON.stringify(rawJSON)); } static async import(kx: ExportContainer): Promise { - const rawKey = JSON.parse(kx.payload); - const key = await parseJWK(rawKey, []); - return new BackupKey(key, kx.id); + const json = JSON.parse(kx.payload); + const key = await parseJWK(json.parsedKey, []); + return new BackupKey(key, kx.id, json.attObj); } static async get(): Promise { @@ -267,11 +271,8 @@ async function parseJWK(jwk, usages): Promise { } export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - let compatibleKey = await getCompatibleKeyFromCryptoKey(backupKey.key); - const coseKey = await compatibleKey.toCOSE(backupKey.key); - let encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); - - let extOutput = new Map([[PSK, encodedKey]]); + let stpMsg = CBOR.encodeCanonical({att_obj: backupKey.attObj}); + let extOutput = new Map([[PSK, stpMsg]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } @@ -365,7 +366,8 @@ export const recover = async ( const signature = await rkPrv.sign(concatData); - return { // ToDo Make getClientExtensionResults work + return { + getClientExtensionResults: () => ({}), id: encRkId, rawId: rkId, response: { @@ -375,5 +377,5 @@ export const recover = async ( userHandle: new ArrayBuffer(0), // This should be nullable }, type: 'public-key', - } as Credential; + } as PublicKeyCredential; }; \ No newline at end of file diff --git a/src/webauthn.ts b/src/webauthn.ts index 8198374..314da9b 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -53,7 +53,6 @@ export const processCredentialCreation = async ( const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId, extOutput); // ToDo Add support for credential counter - const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, @@ -70,7 +69,7 @@ export const processCredentialCreation = async ( log.debug('Attestation created'); return { - getClientExtensionResults: () => ({}), // ToDo Put PSK extension data + getClientExtensionResults: () => ({}), id: encCredId, rawId: credId, response: { @@ -137,6 +136,7 @@ export const processCredentialRequest = async ( log.debug('clientData', clientData); return { + getClientExtensionResults: () => ({}), id: encCredId, rawId: credId, response: { @@ -146,5 +146,5 @@ export const processCredentialRequest = async ( userHandle: new ArrayBuffer(0), }, type: 'public-key', - } as Credential; + } as PublicKeyCredential; }; From 77da7c2b699e3cb66978dd80d1bdb58e8bc213ae Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 29 Jul 2020 18:05:31 +0200 Subject: [PATCH 26/81] Fix encoding problems of BD attestation object --- src/recovery.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/recovery.ts b/src/recovery.ts index a8a1078..0eeea6f 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -104,6 +104,7 @@ export class BackupKey { async export(): Promise { const jwk = await window.crypto.subtle.exportKey("jwk", this.key); + log.debug('Encoded x: ', jwk.x); const rawJSON = {parsedKey: jwk, attObj: this.attObj}; return new ExportContainer(this.id, JSON.stringify(rawJSON)); } @@ -271,7 +272,7 @@ async function parseJWK(jwk, usages): Promise { } export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - let stpMsg = CBOR.encodeCanonical({att_obj: backupKey.attObj}); + let stpMsg = CBOR.encodeCanonical({att_obj: base64ToByteArray(backupKey.attObj, true)}); let extOutput = new Map([[PSK, stpMsg]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } From c7d2bdbe01f112c1ef6c56a9c0b5281a34230a2a Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 29 Jul 2020 20:10:07 +0200 Subject: [PATCH 27/81] Clean up --- src/recovery.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 0eeea6f..1c7fddc 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -20,7 +20,7 @@ export async function pskSetup () { const authId = prompt("Please enter a name for your authenticator", "MyAuth"); const keyAmount: number = +prompt("How many backup keys should be created?", "5"); - await axios.default.post(BackupDeviceBaseUrl + '/setup', {auth_id: authId, key_amount: keyAmount}) + await axios.default.post(BackupDeviceBaseUrl + '/setup', {authId: authId, keyAmount: keyAmount}) .then(async function (response) { log.debug(response) const stpRsp = response.data; @@ -28,7 +28,7 @@ export async function pskSetup () { const container = new Array(); for (i = 0; i < stpRsp.length; ++i) { const jwk = stpRsp[i].jwk; - const attObj = stpRsp[i].att_obj; + const attObj = stpRsp[i].attObj; const parsedKey = await parseJWK(jwk, []); const id = base64ToByteArray(jwk.kid, true); const encId = byteArrayToBase64(id, true); @@ -51,11 +51,11 @@ export async function pskRecovery () { await axios.default.get(BackupDeviceBaseUrl + '/recovery?authId=' + authId) .then(async function (response1) { log.debug(response1); - const keyAmount = response1.data.key_amount; + const keyAmount = response1.data.keyAmount; const rkPub = await RecoveryKey.generate(keyAmount); - await axios.default.post(BackupDeviceBaseUrl + '/recovery', {recovery_keys: rkPub, auth_id: authId}) + await axios.default.post(BackupDeviceBaseUrl + '/recovery', {recoveryKeys: rkPub, authId: authId}) .then(async function (response2) { log.debug(response2); const rawDelegations = response2.data; @@ -64,9 +64,9 @@ export async function pskRecovery () { const container = new Array(); for (i = 0; i < rawDelegations.length; ++i) { const sign = rawDelegations[i].sign; - const srcCredId = base64ToByteArray(rawDelegations[i].src_cred_id, true); + const srcCredId = base64ToByteArray(rawDelegations[i].srcCredId, true); const encSrcCredId = byteArrayToBase64(srcCredId, true); - const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pub_rk); + const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pubRk); container.push(del.export()); } log.debug("Loaded delegation", container); @@ -104,7 +104,6 @@ export class BackupKey { async export(): Promise { const jwk = await window.crypto.subtle.exportKey("jwk", this.key); - log.debug('Encoded x: ', jwk.x); const rawJSON = {parsedKey: jwk, attObj: this.attObj}; return new ExportContainer(this.id, JSON.stringify(rawJSON)); } @@ -272,7 +271,7 @@ async function parseJWK(jwk, usages): Promise { } export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - let stpMsg = CBOR.encodeCanonical({att_obj: base64ToByteArray(backupKey.attObj, true)}); + let stpMsg = CBOR.encodeCanonical({attObj: base64ToByteArray(backupKey.attObj, true)}); let extOutput = new Map([[PSK, stpMsg]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } From 0f4bea9cd23f015872132ef5ad4b058c48175a25 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 30 Jul 2020 15:15:44 +0200 Subject: [PATCH 28/81] Adapt recovery flow --- src/recovery.ts | 119 +++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 73 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 1c7fddc..3b93d4c 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,6 +1,6 @@ import * as CBOR from 'cbor'; import {getLogger} from './logging'; -import {getCompatibleKeyFromCryptoKey, ICOSECompatibleKey} from './crypto'; +import {getCompatibleKeyFromCryptoKey} from './crypto'; import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; import * as axios from "axios"; @@ -53,9 +53,9 @@ export async function pskRecovery () { log.debug(response1); const keyAmount = response1.data.keyAmount; - const rkPub = await RecoveryKey.generate(keyAmount); + const rkData = await RecoveryKey.generate(keyAmount); - await axios.default.post(BackupDeviceBaseUrl + '/recovery', {recoveryKeys: rkPub, authId: authId}) + await axios.default.post(BackupDeviceBaseUrl + '/recovery', {rkData: rkData, authId: authId}) .then(async function (response2) { log.debug(response2); const rawDelegations = response2.data; @@ -63,10 +63,13 @@ export async function pskRecovery () { let i; const container = new Array(); for (i = 0; i < rawDelegations.length; ++i) { - const sign = rawDelegations[i].sign; + const sign = base64ToByteArray(rawDelegations[i].sign, true); + const encSign = byteArrayToBase64(sign, true); const srcCredId = base64ToByteArray(rawDelegations[i].srcCredId, true); const encSrcCredId = byteArrayToBase64(srcCredId, true); - const del = new Delegation(sign, encSrcCredId, rawDelegations[i].pubRk); + const dstCredId = base64ToByteArray(rawDelegations[i].dstCredId, true); + const encDstCredId = byteArrayToBase64(dstCredId, true); + const del = new Delegation(encSign, encSrcCredId, encDstCredId); container.push(del.export()); } log.debug("Loaded delegation", container); @@ -129,18 +132,18 @@ export class BackupKey { export class RecoveryKey { key: CryptoKey; id: string; - backupKey: BackupKey; + attObj: Uint8Array; - constructor(key: CryptoKey, backupKey: BackupKey) { - this.id = backupKey.id; - this.backupKey = backupKey; + constructor(id: string, key: CryptoKey, attObj: Uint8Array) { + this.id = id; this.key = key; + this.attObj = attObj; } async export(): Promise { const parsedKey = await window.crypto.subtle.exportKey("jwk", this.key); - const expBackupKey = await this.backupKey.export(); - const rawJSON = {parsedKey: parsedKey, parsedBackupKey: expBackupKey}; + const parsedAttObj = byteArrayToBase64(this.attObj, true); + const rawJSON = {parsedKey: parsedKey, parsedAttObj: parsedAttObj}; return new ExportContainer(this.id, JSON.stringify(rawJSON)); } @@ -148,13 +151,13 @@ export class RecoveryKey { static async import(kx: ExportContainer): Promise { const json = JSON.parse(kx.payload); const key = await parseJWK(json.parsedKey, ['sign']); - const backupKey = await BackupKey.import(json.parsedBackupKey); + const attObj = base64ToByteArray(json.parsedAttObj, true); - return new RecoveryKey(key, backupKey); + return new RecoveryKey(kx.id, key, attObj); } - static async generate(n: number): Promise> { - const jwk = new Array(); + static async generate(n: number): Promise> { + const delSetup = new Array(); const container = new Array(); let i; for (i = 0; i < n; ++i) { @@ -163,32 +166,38 @@ export class RecoveryKey { true, ['sign'], ); - const bckKey = await BackupKey.get(); - const rk = new RecoveryKey(keyPair.privateKey, bckKey); - const exportRk = await rk.export(); - const pubJWK: any = await window.crypto.subtle.exportKey("jwk", keyPair.publicKey); - pubJWK.kid = rk.id; + const bckKey = await BackupKey.get(); + const pubRk = await getCompatibleKeyFromCryptoKey(keyPair.publicKey); + const pskSetup = await createPSKSetupExtensionOutput(bckKey); + const authData = await pubRk.generateAuthenticatorData("", 0, base64ToByteArray(bckKey.id, true), pskSetup); + const attObj = CBOR.encodeCanonical({ + attStmt: new Map(), + authData: authData, + fmt: 'none', + }); + + const exportRk = await (new RecoveryKey(bckKey.id, keyPair.privateKey, attObj)).export(); container.push(exportRk); - jwk.push(pubJWK); + + delSetup.push(new ExportContainer(exportRk.id, byteArrayToBase64(attObj, true))); + log.debug("AttObj:", byteArrayToBase64(attObj, true)); } await saveExportContainer(RECOVERY, container); - return jwk; + return delSetup; } } class Delegation { sign: string; srcCredId: string; - rkId: string; - pubRK: JsonWebKey; - constructor(sign, srcCredId, jwk) { + dstCredId: string; + constructor(sign, srcCredId, dstCredId) { this.srcCredId = srcCredId; this.sign = sign; - this.rkId = jwk.kid; - this.pubRK = jwk; + this.dstCredId = dstCredId; } export(): ExportContainer { @@ -207,52 +216,19 @@ class Delegation { } class RecoveryMessage { - srcCredId: string; - delSign: Uint8Array; - pubRK: Uint8Array; - attestationObject: Uint8Array; - clientDataJSON: Uint8Array; - - constructor() { - // Dummy - } - - async init(delegation: Delegation, rkPub: ICOSECompatibleKey, backupKey: BackupKey, origin: string, challenge: ArrayBuffer) { - this.srcCredId = delegation.srcCredId; - this.delSign = base64ToByteArray(delegation.sign, true); - - // Create attestation object for new key - const recoveryCredId = base64ToByteArray(delegation.rkId, true); - - const pskExtOutput = await createPSKSetupExtensionOutput(backupKey); - const authData = await rkPub.generateAuthenticatorData(origin, 0, recoveryCredId, pskExtOutput); - - const coseKey = await rkPub.toCOSE(rkPub.publicKey); - this.pubRK = new Uint8Array(CBOR.encodeCanonical(coseKey)); - - this.attestationObject = CBOR.encodeCanonical({ - attStmt: new Map(), - authData: authData, - fmt: 'none', - }); - - - const clientData = await rkPub.generateClientData( - challenge, - { origin, type: 'webauthn.create' }, - ); - this.clientDataJSON = base64ToByteArray(window.btoa(clientData), true); + del: Delegation; + rk: RecoveryKey; + constructor(delegation: Delegation, rk: RecoveryKey) { + this.del = delegation; + this.rk = rk; } encode(): ArrayBuffer { return CBOR.encodeCanonical({ - delSign: byteArrayToBase64(this.delSign, true), - srcCredId: this.srcCredId, - authAttData: { - clientDataJSON: this.clientDataJSON, - attestationObject: this.attestationObject - }, + delSign: this.del.sign, + srcCredId: this.del.srcCredId, + attestationObject: this.rk.attObj }).buffer; } } @@ -303,7 +279,7 @@ class RecoveryOptions { async function getRecoveryOptions(srcCredId: string): Promise { const del = await Delegation.getById(srcCredId); log.debug('Use delegation', del); - const rk = await getRecoveryKey(del.rkId); + const rk = await getRecoveryKey(del.dstCredId); log.debug('Use recovery key', rk); return new RecoveryOptions(rk, del); } @@ -333,11 +309,8 @@ export const recover = async ( const encRkId = byteArrayToBase64(rkId, true); const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.rk.key); - const rawRKPub = await parseJWK(recOps.del.pubRK, []); - const rkPub = await getCompatibleKeyFromCryptoKey(rawRKPub); - const recMessage = new RecoveryMessage(); - await recMessage.init(recOps.del, rkPub, recOps.rk.backupKey, origin, publicKeyRequestOptions.challenge as ArrayBuffer); + const recMessage = new RecoveryMessage(recOps.del, recOps.rk); log.debug('Recovery message', recMessage); const extOutput = await createPSKRecoveryExtensionOutput(recMessage); From 2e206aa123583ef3945d54793a530e73d1359a17 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 31 Jul 2020 12:36:18 +0200 Subject: [PATCH 29/81] Fix storage problems --- src/recovery.ts | 1 - src/storage.ts | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 3b93d4c..d4c559d 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -181,7 +181,6 @@ export class RecoveryKey { container.push(exportRk); delSetup.push(new ExportContainer(exportRk.id, byteArrayToBase64(attObj, true))); - log.debug("AttObj:", byteArrayToBase64(attObj, true)); } await saveExportContainer(RECOVERY, container); diff --git a/src/storage.ts b/src/storage.ts index 27f15c8..cbc8991 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -54,8 +54,9 @@ export async function saveExportContainer(cType: ExportContainerType, container: log.debug(`Storing ${cType} container`, exportJSON); return new Promise(async (res, rej) => { - chrome.storage.sync.set({[cType]: exportJSON}, () => { + chrome.storage.local.set({[cType]: exportJSON}, () => { if (!!chrome.runtime.lastError) { + log.error('Could not store container', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); } else { res(); @@ -66,7 +67,7 @@ export async function saveExportContainer(cType: ExportContainerType, container: export async function fetchExportContainer(cType: ExportContainerType): Promise> { return new Promise>(async (res, rej) => { - chrome.storage.sync.get({[cType]: null}, async (resp) => { + chrome.storage.local.get({[cType]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.warn(`Could not fetch ${cType} container`); rej(chrome.runtime.lastError); From 2a2827bb2b19a7b6f61f12cde10ca9642c825f08 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 31 Jul 2020 15:17:46 +0200 Subject: [PATCH 30/81] Add padding during recovery request --- src/recovery.ts | 4 ++-- src/utils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index d4c559d..8719a83 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,7 +1,7 @@ import * as CBOR from 'cbor'; import {getLogger} from './logging'; import {getCompatibleKeyFromCryptoKey} from './crypto'; -import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; +import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin, padString} from './utils'; import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; import * as axios from "axios"; @@ -180,7 +180,7 @@ export class RecoveryKey { const exportRk = await (new RecoveryKey(bckKey.id, keyPair.privateKey, attObj)).export(); container.push(exportRk); - delSetup.push(new ExportContainer(exportRk.id, byteArrayToBase64(attObj, true))); + delSetup.push(new ExportContainer(exportRk.id, padString(byteArrayToBase64(attObj, true)))); } await saveExportContainer(RECOVERY, container); diff --git a/src/utils.ts b/src/utils.ts index 362cd90..e3cf50a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -85,7 +85,7 @@ export function base64ToByteArray(str: string, urlEncoded: boolean = false): Uin return Uint8Array.from(atob(rawInput), (c) => c.charCodeAt(0)); } -function padString(input: string): string { +export function padString(input: string): string { let result = input; while (result.length % 4) { result += '='; From f6e25aeaf2edea19d70c2da2efa82ba89b181fa2 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 1 Aug 2020 12:54:52 +0200 Subject: [PATCH 31/81] Handle all credentials inside PublicKeyRequestOptions --- src/recovery.ts | 32 ++++++++++++++++++++++++++------ src/webauthn.ts | 25 +++++++++++++++++-------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 8719a83..23ba474 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -278,8 +278,14 @@ class RecoveryOptions { async function getRecoveryOptions(srcCredId: string): Promise { const del = await Delegation.getById(srcCredId); log.debug('Use delegation', del); + if (del == null) { + return null; + } const rk = await getRecoveryKey(del.dstCredId); log.debug('Use recovery key', rk); + if (rk == null) { + return null; + } return new RecoveryOptions(rk, del); } @@ -295,13 +301,27 @@ export const recover = async ( return null; } - // For now we will only worry about the first entry - const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; - const srcCredId: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const encSrcCredId = byteArrayToBase64(new Uint8Array(srcCredId), true); - log.info('Started recovery for', encSrcCredId); + let srcCredId: ArrayBuffer; + let encSrcCredId; + let i; + let recOps; + let requestedCredential; + for (i = 0; i < publicKeyRequestOptions.allowCredentials.length; i++) { + requestedCredential = publicKeyRequestOptions.allowCredentials[i]; + srcCredId = requestedCredential.id as ArrayBuffer; + encSrcCredId = byteArrayToBase64(new Uint8Array(srcCredId), true); + + recOps = await getRecoveryOptions(encSrcCredId); - const recOps = await getRecoveryOptions(encSrcCredId); + if (recOps != null) { + break; + } + } + if (!recOps) { + throw new Error(`no recovery options available for credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)}`); + } + + log.info('Started recovery for', encSrcCredId); log.debug('Recovery options', recOps); const rkId = base64ToByteArray(recOps.rk.id, true); diff --git a/src/webauthn.ts b/src/webauthn.ts index 314da9b..331ce71 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -52,7 +52,6 @@ export const processCredentialCreation = async ( } const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId, extOutput); - // ToDo Add support for credential counter const clientData = await compatibleKey.generateClientData( publicKeyCreationOptions.challenge as ArrayBuffer, { origin, type: 'webauthn.create' }, @@ -99,16 +98,27 @@ export const processCredentialRequest = async ( } } - const requestedCredential = publicKeyRequestOptions.allowCredentials[0]; // ToDo Handle all entries - const credId: ArrayBuffer = requestedCredential.id as ArrayBuffer; - const encCredId = byteArrayToBase64(new Uint8Array(credId), true); - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + let i; + let key; + let credId: ArrayBuffer; + let encCredId; + for (i = 0; i < publicKeyRequestOptions.allowCredentials.length; i++) { + const requestedCredential = publicKeyRequestOptions.allowCredentials[i]; + credId = requestedCredential.id as ArrayBuffer; + encCredId = byteArrayToBase64(new Uint8Array(credId), true); - const key = await fetchKey(encCredId, pin); + key = await fetchKey(encCredId, pin).catch(_ => null); + if (key) { + break; + } + } if (!key) { - throw new Error(`credential with id ${encCredId} not found`); + throw new Error(`no credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)} not found`); } + + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + const compatibleKey = await getCompatibleKeyFromCryptoKey(key); const clientData = await compatibleKey.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, @@ -123,7 +133,6 @@ export const processCredentialRequest = async ( const clientDataJSON = base64ToByteArray(window.btoa(clientData)); const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); - // ToDo Update counter const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); // Prepare input for signature From f9785b51da778f3f51d020c23615a53a4b8da240 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 1 Aug 2020 14:32:04 +0200 Subject: [PATCH 32/81] Code style --- src/background.ts | 17 ++-- src/crypto.ts | 31 ++++--- src/popup.ts | 2 - src/recovery.ts | 212 +++++++++++++++++++++++----------------------- src/storage.ts | 21 +++-- src/utils.ts | 6 +- src/webauthn.ts | 27 +++--- 7 files changed, 162 insertions(+), 154 deletions(-) diff --git a/src/background.ts b/src/background.ts index a961642..2e98f0f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,9 +1,12 @@ import {disabledIcons, enabledIcons} from './constants'; + import {getLogger} from './logging'; + import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; -import {processCredentialRequest, processCredentialCreation} from './webauthn'; -import {BackupDeviceBaseUrl, RecoveryKey, pskSetup, pskRecovery} from "./recovery"; -import * as axios from 'axios'; + +import {processCredentialCreation, processCredentialRequest} from './webauthn'; + +import {pskRecovery, pskSetup} from './recovery'; const log = getLogger('background'); @@ -41,9 +44,9 @@ const setup = async () => { }; const recovery = async () => { - log.debug('Recovery called!') + log.debug('Recovery called!'); await pskRecovery(); -} +}; const create = async (msg, sender: chrome.runtime.MessageSender) => { if (!sender.tab || !sender.tab.id) { @@ -116,10 +119,10 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } break; case 'setup': - setup().then(() => alert("Backup keys synchronized successfully!")); + setup().then(() => alert('Backup keys synchronized successfully!')); break; case 'recovery': - recovery().then(() => alert("Recovery finished successfully!")) + recovery().then(() => alert('Recovery finished successfully!')); break; default: sendResponse(null); diff --git a/src/crypto.ts b/src/crypto.ts index 85010f5..9ed1ce2 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,8 +1,5 @@ import * as CBOR from 'cbor'; -import { getLogger } from './logging'; -import { base64ToByteArray, byteArrayToBase64 } from './utils'; - -const log = getLogger('crypto'); +import {base64ToByteArray, byteArrayToBase64} from './utils'; // Copied from krypton function counterToBytes(c: number): Uint8Array { @@ -30,7 +27,8 @@ export interface ICOSECompatibleKey { privateKey?: CryptoKey; publicKey?: CryptoKey; generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; - generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, extensionOutput: Uint8Array): Promise; + generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, + extensionOutput: Uint8Array): Promise; sign(clientData: Uint8Array): Promise; toCOSE(key: CryptoKey): Promise>; } @@ -38,7 +36,7 @@ export interface ICOSECompatibleKey { class ECDSA implements ICOSECompatibleKey { public static async fromKey(key: CryptoKey): Promise { - if (key.type === "public") { + if (key.type === 'public') { return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], null, key); } else { return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], key); @@ -91,7 +89,8 @@ class ECDSA implements ICOSECompatibleKey { }); } - public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, extensionOutput: Uint8Array = null): Promise { + public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, + extensionOutput: Uint8Array = null): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); @@ -183,28 +182,24 @@ class ECDSA implements ICOSECompatibleKey { this.getKeyParams(), this.privateKey, data, - ) + ); - const rawSig = new Buffer(tmpSign) + const rawSig = new Buffer(tmpSign); // Credit to: https://stackoverflow.com/a/39651457/5333936 const asn1 = require('asn1.js'); const BN = require('bn.js'); - const EcdsaDerSig = asn1.define('ECPrivateKey', function() { + const ECDSA_DER_SIG = asn1.define('ECPrivateKey', function() { return this.seq().obj( this.key('r').int(), - this.key('s').int() + this.key('s').int(), ); }); const r = new BN(rawSig.slice(0, 32).toString('hex'), 16, 'be'); const s = new BN(rawSig.slice(32).toString('hex'), 16, 'be'); - return EcdsaDerSig.encode({r, s}, 'der'); - } - - private getKeyParams(): EcdsaParams { - return { name: 'ECDSA', hash: coseEllipticCurveNames[ECDSA.ellipticCurveKeys[this.algorithm]] }; + return ECDSA_DER_SIG.encode({r, s}, 'der'); } public async toCOSE(key: CryptoKey): Promise> { @@ -220,6 +215,10 @@ class ECDSA implements ICOSECompatibleKey { attData.set(-3, base64ToByteArray(exportedKey.y, true)); return attData; } + + private getKeyParams(): EcdsaParams { + return { name: 'ECDSA', hash: coseEllipticCurveNames[ECDSA.ellipticCurveKeys[this.algorithm]] }; + } } // ECDSA w/ SHA-256 diff --git a/src/popup.ts b/src/popup.ts index 082a631..4b76401 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -3,7 +3,6 @@ import { getLogger } from './logging'; const log = getLogger('popup'); - $(() => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => { const currentTab = tabs.find((t) => !!t.id); @@ -25,7 +24,6 @@ $(() => { }); }); - const tabKey = `tab-${currentTab.id}`; chrome.storage.local.get([tabKey], (result) => { log.debug('got storage results', result); diff --git a/src/recovery.ts b/src/recovery.ts index 23ba474..ef13fbf 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -1,28 +1,32 @@ +import * as axios from 'axios'; import * as CBOR from 'cbor'; -import {getLogger} from './logging'; -import {getCompatibleKeyFromCryptoKey} from './crypto'; + import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin, padString} from './utils'; + import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; -import * as axios from "axios"; + +import {getCompatibleKeyFromCryptoKey} from './crypto'; + +import {getLogger} from './logging'; const log = getLogger('recovery'); -export const PSK: string = 'psk' +export const PSK: string = 'psk'; -export const BackupDeviceBaseUrl = 'http://localhost:8005' // ToDo Load from a config file +export const BACKUP_DEVICE_BASE_URL = 'http://localhost:8005'; // ToDo Load from a config file -export type ExportContainerType = string -const BACKUP: ExportContainerType = 'backup' -const RECOVERY: ExportContainerType = 'recovery' -const DELEGATION: ExportContainerType = 'delegation' +export type ExportContainerType = string; +const BACKUP: ExportContainerType = 'backup'; +const RECOVERY: ExportContainerType = 'recovery'; +const DELEGATION: ExportContainerType = 'delegation'; -export async function pskSetup () { - const authId = prompt("Please enter a name for your authenticator", "MyAuth"); - const keyAmount: number = +prompt("How many backup keys should be created?", "5"); +export async function pskSetup() { + const authId = prompt('Please enter a name for your authenticator', 'MyAuth'); + const keyAmount: number = +prompt('How many backup keys should be created?', '5'); - await axios.default.post(BackupDeviceBaseUrl + '/setup', {authId: authId, keyAmount: keyAmount}) - .then(async function (response) { - log.debug(response) + await axios.default.post(BACKUP_DEVICE_BASE_URL + '/setup', {authId, keyAmount}) + .then(async function(response) { + log.debug(response); const stpRsp = response.data; let i; const container = new Array(); @@ -40,23 +44,23 @@ export async function pskSetup () { await saveExportContainer(BACKUP, container); }) - .catch(function (error) { + .catch(function(error) { log.error(error); - }) + }); } -export async function pskRecovery () { - const authId = prompt("Which authenticator you want to replace?", "MyAuth"); +export async function pskRecovery() { + const authId = prompt('Which authenticator you want to replace?', 'MyAuth'); - await axios.default.get(BackupDeviceBaseUrl + '/recovery?authId=' + authId) - .then(async function (response1) { + await axios.default.get(BACKUP_DEVICE_BASE_URL + '/recovery?authId=' + authId) + .then(async function(response1) { log.debug(response1); const keyAmount = response1.data.keyAmount; const rkData = await RecoveryKey.generate(keyAmount); - await axios.default.post(BackupDeviceBaseUrl + '/recovery', {rkData: rkData, authId: authId}) - .then(async function (response2) { + await axios.default.post(BACKUP_DEVICE_BASE_URL + '/recovery', {rkData, authId}) + .then(async function(response2) { log.debug(response2); const rawDelegations = response2.data; @@ -72,21 +76,21 @@ export async function pskRecovery () { const del = new Delegation(encSign, encSrcCredId, encDstCredId); container.push(del.export()); } - log.debug("Loaded delegation", container); + log.debug('Loaded delegation', container); await saveExportContainer(DELEGATION, container); }) - .catch(function (error) { + .catch(function(error) { log.error(error); - }) + }); }) - .catch(function (error) { + .catch(function(error) { log.error(error); - }) + }); } export class ExportContainer { - id: string; - payload: string; + public id: string; + public payload: string; constructor(id: string, payload: string) { this.id = id; @@ -95,30 +99,15 @@ export class ExportContainer { } export class BackupKey { - key: CryptoKey; - attObj: string; - id: string; - - constructor(key: CryptoKey, id: string, attObj: string) { - this.key = key; - this.id = id; - this.attObj = attObj; - } - - async export(): Promise { - const jwk = await window.crypto.subtle.exportKey("jwk", this.key); - const rawJSON = {parsedKey: jwk, attObj: this.attObj}; - return new ExportContainer(this.id, JSON.stringify(rawJSON)); - } - static async import(kx: ExportContainer): Promise { + public static async import(kx: ExportContainer): Promise { const json = JSON.parse(kx.payload); const key = await parseJWK(json.parsedKey, []); return new BackupKey(key, kx.id, json.attObj); } - static async get(): Promise { + public static async get(): Promise { const container = await fetchExportContainer(BACKUP); - if (container.length == 0) { + if (container.length === 0) { throw new Error(`No backup key available`); } const key = container.pop(); @@ -127,28 +116,26 @@ export class BackupKey { return await BackupKey.import(key); } -} -export class RecoveryKey { - key: CryptoKey; - id: string; - attObj: Uint8Array; + public key: CryptoKey; + public attObj: string; + public id: string; - constructor(id: string, key: CryptoKey, attObj: Uint8Array) { - this.id = id; + constructor(key: CryptoKey, id: string, attObj: string) { this.key = key; + this.id = id; this.attObj = attObj; } - async export(): Promise { - const parsedKey = await window.crypto.subtle.exportKey("jwk", this.key); - const parsedAttObj = byteArrayToBase64(this.attObj, true); - const rawJSON = {parsedKey: parsedKey, parsedAttObj: parsedAttObj}; - + public async export(): Promise { + const jwk = await window.crypto.subtle.exportKey('jwk', this.key); + const rawJSON = {parsedKey: jwk, attObj: this.attObj}; return new ExportContainer(this.id, JSON.stringify(rawJSON)); } +} - static async import(kx: ExportContainer): Promise { +export class RecoveryKey { + public static async import(kx: ExportContainer): Promise { const json = JSON.parse(kx.payload); const key = await parseJWK(json.parsedKey, ['sign']); const attObj = base64ToByteArray(json.parsedAttObj, true); @@ -156,13 +143,13 @@ export class RecoveryKey { return new RecoveryKey(kx.id, key, attObj); } - static async generate(n: number): Promise> { + public static async generate(n: number): Promise { const delSetup = new Array(); const container = new Array(); let i; for (i = 0; i < n; ++i) { const keyPair = await window.crypto.subtle.generateKey( - { name: 'ECDSA', namedCurve: "P-256" }, + { name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign'], ); @@ -170,10 +157,10 @@ export class RecoveryKey { const bckKey = await BackupKey.get(); const pubRk = await getCompatibleKeyFromCryptoKey(keyPair.publicKey); const pskSetup = await createPSKSetupExtensionOutput(bckKey); - const authData = await pubRk.generateAuthenticatorData("", 0, base64ToByteArray(bckKey.id, true), pskSetup); + const authData = await pubRk.generateAuthenticatorData('', 0, base64ToByteArray(bckKey.id, true), pskSetup); const attObj = CBOR.encodeCanonical({ attStmt: new Map(), - authData: authData, + authData, fmt: 'none', }); @@ -187,87 +174,105 @@ export class RecoveryKey { return delSetup; } + + public key: CryptoKey; + public id: string; + public attObj: Uint8Array; + + constructor(id: string, key: CryptoKey, attObj: Uint8Array) { + this.id = id; + this.key = key; + this.attObj = attObj; + } + + public async export(): Promise { + const parsedKey = await window.crypto.subtle.exportKey('jwk', this.key); + const parsedAttObj = byteArrayToBase64(this.attObj, true); + const rawJSON = {parsedKey, parsedAttObj}; + + return new ExportContainer(this.id, JSON.stringify(rawJSON)); + } } class Delegation { - sign: string; - srcCredId: string; - dstCredId: string; + public static import(kx: ExportContainer): Delegation { + return JSON.parse(kx.payload); + } + + public static async getById(srcCredId: string): Promise { + const container = await fetchExportContainer(DELEGATION); + log.debug('Fetched delegations', container); + const del = container.filter((x) => x.id === srcCredId); + return del.length !== 0 ? (Delegation.import(del[0])) : null; + } + + public sign: string; + public srcCredId: string; + public dstCredId: string; + constructor(sign, srcCredId, dstCredId) { this.srcCredId = srcCredId; this.sign = sign; this.dstCredId = dstCredId; } - export(): ExportContainer { + public export(): ExportContainer { return new ExportContainer(this.srcCredId, JSON.stringify(this)); } - static import(kx: ExportContainer): Delegation { - return JSON.parse(kx.payload); - } - - static async getById(srcCredId: string): Promise { - const container = await fetchExportContainer(DELEGATION); - log.debug('Fetched delegations', container); - const del = container.filter(x => x.id === srcCredId); - return del.length != 0 ? (Delegation.import(del[0])) : null; - } } class RecoveryMessage { - del: Delegation; - rk: RecoveryKey; + public del: Delegation; + public rk: RecoveryKey; constructor(delegation: Delegation, rk: RecoveryKey) { this.del = delegation; this.rk = rk; } - encode(): ArrayBuffer { + public encode(): ArrayBuffer { return CBOR.encodeCanonical({ + attestationObject: this.rk.attObj, delSign: this.del.sign, srcCredId: this.del.srcCredId, - attestationObject: this.rk.attObj }).buffer; } } async function parseJWK(jwk, usages): Promise { return window.crypto.subtle.importKey( - "jwk", + 'jwk', jwk, { - name: "ECDSA", - namedCurve: "P-256" + name: 'ECDSA', + namedCurve: 'P-256', }, true, - usages + usages, ); } export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - let stpMsg = CBOR.encodeCanonical({attObj: base64ToByteArray(backupKey.attObj, true)}); - let extOutput = new Map([[PSK, stpMsg]]); + const stpMsg = CBOR.encodeCanonical({attObj: base64ToByteArray(backupKey.attObj, true)}); + const extOutput = new Map([[PSK, stpMsg]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } async function createPSKRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { - let extOutput = new Map([[PSK, recMsg.encode()]]); + const extOutput = new Map([[PSK, recMsg.encode()]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } - - async function getRecoveryKey(id: string): Promise { const container = await fetchExportContainer(RECOVERY); log.debug(container); - const rk = container.filter(x => x.id === id); - return rk.length != 0 ? (await RecoveryKey.import(rk[0])) : null; + const rk = container.filter((x) => x.id === id); + return rk.length !== 0 ? (await RecoveryKey.import(rk[0])) : null; } class RecoveryOptions { - rk: RecoveryKey; - del: Delegation; + public rk: RecoveryKey; + public del: Delegation; constructor(rk: RecoveryKey, del: Delegation) { this.del = del; @@ -278,19 +283,17 @@ class RecoveryOptions { async function getRecoveryOptions(srcCredId: string): Promise { const del = await Delegation.getById(srcCredId); log.debug('Use delegation', del); - if (del == null) { + if (del === null) { return null; } const rk = await getRecoveryKey(del.dstCredId); log.debug('Use recovery key', rk); - if (rk == null) { + if (rk === null) { return null; } return new RecoveryOptions(rk, del); } - -// This function is called when recovery is needed export const recover = async ( origin: string, publicKeyRequestOptions: PublicKeyCredentialRequestOptions, @@ -313,7 +316,7 @@ export const recover = async ( recOps = await getRecoveryOptions(encSrcCredId); - if (recOps != null) { + if (recOps !== null) { break; } } @@ -350,7 +353,8 @@ export const recover = async ( const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); + const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), + new Uint8Array(extOutput)); const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); concatData.set(authenticatorData); @@ -364,10 +368,10 @@ export const recover = async ( rawId: rkId, response: { authenticatorData: authenticatorData.buffer, - clientDataJSON: clientDataJSON, + clientDataJSON, signature: (new Uint8Array(signature)).buffer, userHandle: new ArrayBuffer(0), // This should be nullable }, type: 'public-key', } as PublicKeyCredential; -}; \ No newline at end of file +}; diff --git a/src/storage.ts b/src/storage.ts index cbc8991..344b098 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -1,7 +1,10 @@ import {ivLength, keyExportFormat, saltLength} from './constants'; + import {base64ToByteArray, byteArrayToBase64, concatenate} from './utils'; -import {getLogger} from "./logging"; -import {ExportContainer, ExportContainerType} from "./recovery"; + +import {getLogger} from './logging'; + +import {ExportContainer, ExportContainerType} from './recovery'; const log = getLogger('storage'); @@ -48,8 +51,8 @@ const getWrappingKey = async (pin: string, salt: Uint8Array): Promise ); }; -export async function saveExportContainer(cType: ExportContainerType, container: Array): Promise { - let exportJSON = JSON.stringify(container); +export async function saveExportContainer(cType: ExportContainerType, container: ExportContainer[]): Promise { + const exportJSON = JSON.stringify(container); log.debug(`Storing ${cType} container`, exportJSON); @@ -65,8 +68,8 @@ export async function saveExportContainer(cType: ExportContainerType, container: }); } -export async function fetchExportContainer(cType: ExportContainerType): Promise> { - return new Promise>(async (res, rej) => { +export async function fetchExportContainer(cType: ExportContainerType): Promise { + return new Promise(async (res, rej) => { chrome.storage.local.get({[cType]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.warn(`Could not fetch ${cType} container`); @@ -78,8 +81,8 @@ export async function fetchExportContainer(cType: ExportContainerType): Promise< return rej(`Container ${cType} not found`); } - let exportJSON = await JSON.parse(resp[cType]); - let exportContainer = new Array(); + const exportJSON = await JSON.parse(resp[cType]); + const exportContainer = new Array(); let i; for (i = 0; i < exportJSON.length; ++i) { exportContainer.push(new ExportContainer(exportJSON[i].id, exportJSON[i].payload)); @@ -98,7 +101,7 @@ export const fetchKey = async (key: string, pin: string): Promise => return; } if (resp[key] == null) { - return rej("Key not found"); + return rej('Key not found'); } log.info('PIN', pin); const payload = base64ToByteArray(resp[key]); diff --git a/src/utils.ts b/src/utils.ts index e3cf50a..7c1eee5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -69,8 +69,8 @@ export function byteArrayToBase64(arr: Uint8Array, urlEncoded: boolean = false): const result = btoa(String.fromCharCode(...arr)); if (urlEncoded) { return result.replace(/=/g, '') - .replace(/\+/g, "-") - .replace(/\//g, "_"); + .replace(/\+/g, '-') + .replace(/\//g, '_'); } return result; } @@ -91,4 +91,4 @@ export function padString(input: string): string { result += '='; } return result; -} \ No newline at end of file +} diff --git a/src/webauthn.ts b/src/webauthn.ts index 331ce71..2c1ec3b 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,13 +1,14 @@ import * as CBOR from 'cbor'; + import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; -import { getLogger } from './logging'; -import { fetchKey, keyExists, saveKey } from './storage'; -import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './utils'; -import { - BackupKey, - PSK, - createPSKSetupExtensionOutput, recover, -} from "./recovery"; + +import {getLogger} from './logging'; + +import {fetchKey, keyExists, saveKey} from './storage'; + +import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; + +import {BackupKey, createPSKSetupExtensionOutput, PSK, recover} from './recovery'; const log = getLogger('webauthn'); @@ -34,7 +35,7 @@ export const processCredentialCreation = async ( const rp = publicKeyCreationOptions.rp; const rpID = rp.id || getDomainFromOrigin(origin); - let bckpKey = await BackupKey.get(); + const bckpKey = await BackupKey.get(); log.info('Use backup key', bckpKey); const credId = base64ToByteArray(bckpKey.id, true); @@ -44,7 +45,7 @@ export const processCredentialCreation = async ( throw new Error(`credential with id ${encCredId} already exists`); } - let compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); + const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); let extOutput = null; if (supportRecovery) { @@ -107,14 +108,14 @@ export const processCredentialRequest = async ( credId = requestedCredential.id as ArrayBuffer; encCredId = byteArrayToBase64(new Uint8Array(credId), true); - key = await fetchKey(encCredId, pin).catch(_ => null); + key = await fetchKey(encCredId, pin).catch((_) => null); if (key) { break; } } if (!key) { - throw new Error(`no credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)} not found`); + throw new Error(`credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)} not found`); } const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); @@ -150,7 +151,7 @@ export const processCredentialRequest = async ( rawId: credId, response: { authenticatorData: authenticatorData.buffer, - clientDataJSON: clientDataJSON, + clientDataJSON, signature: (new Uint8Array(signature)).buffer, userHandle: new ArrayBuffer(0), }, From 22056e013fd9208d6c5ab5763cd72c7f5cbe6866 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 3 Aug 2020 15:39:15 +0200 Subject: [PATCH 33/81] Add constant AAGUID --- src/crypto.ts | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/crypto.ts b/src/crypto.ts index 9ed1ce2..6d8fa87 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,6 +1,15 @@ +import * as asn1 from 'asn1.js'; +import {BN} from 'bn.js'; import * as CBOR from 'cbor'; import {base64ToByteArray, byteArrayToBase64} from './utils'; +const CKEY_ID = new Uint8Array([ + 1214244733, 1205845608, 840015201, 3897052717, + 4072880437, 4027233456, 675224361, 2305433287, + 74291263, 3461796691, 701523034, 3178201666, + 3992003567, 1410532, 4234129691, 1438515639, +]); + // Copied from krypton function counterToBytes(c: number): Uint8Array { const bytes = new Uint8Array(4); @@ -101,7 +110,7 @@ class ECDSA implements ICOSECompatibleKey { let authenticatorDataLength = rpIdHash.length + 1 + 4; if (this.publicKey) { - aaguid = credentialId.slice(0, 16); + aaguid = CKEY_ID.slice(0, 16); // 16-bit unsigned big-endian integer. credIdLen = new Uint8Array(2); credIdLen[0] = (credentialId.length >> 8) & 0xff; @@ -178,28 +187,24 @@ class ECDSA implements ICOSECompatibleKey { if (!this.privateKey) { throw new Error('no private key available for signing'); } - const tmpSign = await window.crypto.subtle.sign( + const rawSign = await window.crypto.subtle.sign( this.getKeyParams(), this.privateKey, data, ); - const rawSig = new Buffer(tmpSign); + const rawSignBuf = new Buffer(rawSign); // Credit to: https://stackoverflow.com/a/39651457/5333936 - const asn1 = require('asn1.js'); - const BN = require('bn.js'); - - const ECDSA_DER_SIG = asn1.define('ECPrivateKey', function() { + const ecdsaDerSig = asn1.define('ECPrivateKey', function() { return this.seq().obj( this.key('r').int(), this.key('s').int(), ); }); - - const r = new BN(rawSig.slice(0, 32).toString('hex'), 16, 'be'); - const s = new BN(rawSig.slice(32).toString('hex'), 16, 'be'); - return ECDSA_DER_SIG.encode({r, s}, 'der'); + const r = new BN(rawSignBuf.slice(0, 32).toString('hex'), 16, 'be'); + const s = new BN(rawSignBuf.slice(32).toString('hex'), 16, 'be'); + return ecdsaDerSig.encode({r, s}, 'der'); } public async toCOSE(key: CryptoKey): Promise> { From ead3a05222235f33e3911d8ee760c0202193ea14 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 3 Aug 2020 18:00:14 +0200 Subject: [PATCH 34/81] Add separate config page --- dist/chromium/manifest.json | 10 ++- dist/chromium/options.html | 45 +++++++++++++ dist/chromium/popup.html | 5 -- dist/chromium/styles/option.css | 111 ++++++++++++++++++++++++++++++++ src/option.ts | 24 +++++++ src/popup.ts | 14 ---- webpack.common.js | 3 +- 7 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 dist/chromium/options.html create mode 100644 dist/chromium/styles/option.css create mode 100644 src/option.ts diff --git a/dist/chromium/manifest.json b/dist/chromium/manifest.json index 07ec167..c7a85d5 100644 --- a/dist/chromium/manifest.json +++ b/dist/chromium/manifest.json @@ -27,7 +27,12 @@ "js/background.js" ] }, - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", + "options_page": "options.html", + "options_ui": { + "page": "options.html", + "open_in_tab": false + }, + "content_security_policy": "script-src 'self' https://code.jquery.com https://cdnjs.cloudflare.com https://cdn.jsdelivr.net https://stackpath.bootstrapcdn.com 'unsafe-eval'; object-src 'self'", "page_action": { "default_icon": { "16": "images/lock-16.png", @@ -50,7 +55,6 @@ ], "web_accessible_resources": [ "js/inject_webauthn.js", - "img/*", - "recovery/*" + "img/*" ] } \ No newline at end of file diff --git a/dist/chromium/options.html b/dist/chromium/options.html new file mode 100644 index 0000000..40455c3 --- /dev/null +++ b/dist/chromium/options.html @@ -0,0 +1,45 @@ + + + + + Identity Manager + + + + + + + + + + + + + + + + +
+ +
+

PSK Options

+

+ + +

+
+ Backup Device Contact URL + +
+
+ +
+
+

© 2020

+
+
+
+ + + + \ No newline at end of file diff --git a/dist/chromium/popup.html b/dist/chromium/popup.html index f371d9b..29e9940 100644 --- a/dist/chromium/popup.html +++ b/dist/chromium/popup.html @@ -17,11 +17,6 @@ -
- PSK Options - - -
\ No newline at end of file diff --git a/dist/chromium/styles/option.css b/dist/chromium/styles/option.css new file mode 100644 index 0000000..baf1d29 --- /dev/null +++ b/dist/chromium/styles/option.css @@ -0,0 +1,111 @@ +/* + * Globals + */ + +/* Links */ +a, +a:focus, +a:hover { + color: #fff; +} + +/* Custom default button */ +.btn-secondary, +.btn-secondary:hover, +.btn-secondary:focus { + color: #333; + text-shadow: none; /* Prevent inheritance from `body` */ + background-color: #fff; + border: .05rem solid #fff; +} + + +/* + * Base structure + */ + +html, +body { + height: 100%; + background-color: #333; +} + +body { + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + color: #fff; + text-shadow: 0 .05rem .1rem rgba(0, 0, 0, .5); + box-shadow: inset 0 0 5rem rgba(0, 0, 0, .5); +} + +.cover-container { + max-width: 42em; +} + + +/* + * Header + */ +.masthead { + margin-bottom: 2rem; +} + +.masthead-brand { + margin-bottom: 0; +} + +.nav-masthead .nav-link { + padding: .25rem 0; + font-weight: 700; + color: rgba(255, 255, 255, .5); + background-color: transparent; + border-bottom: .25rem solid transparent; +} + +.nav-masthead .nav-link:hover, +.nav-masthead .nav-link:focus { + border-bottom-color: rgba(255, 255, 255, .25); +} + +.nav-masthead .nav-link + .nav-link { + margin-left: 1rem; +} + +.nav-masthead .active { + color: #fff; + border-bottom-color: #fff; +} + +@media (min-width: 250px) { + .masthead-brand { + float: left; + } + .nav-masthead { + float: right; + } +} + + +/* + * Cover + */ +.cover { + padding: 0 1.5rem; +} +.cover .btn-lg { + padding: .75rem 1.25rem; + font-weight: 700; +} + + +/* + * Footer + */ +.mastfoot { + color: rgba(255, 255, 255, .5); +} + diff --git a/src/option.ts b/src/option.ts new file mode 100644 index 0000000..34edec5 --- /dev/null +++ b/src/option.ts @@ -0,0 +1,24 @@ +import $ from 'jquery'; + +$(() => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => { + const currentTab = tabs.find((t) => !!t.id); + if (!currentTab) { + return; + } + + $('#Setup').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'setup', + }); + }); + + $('#Recovery').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'recovery', + }); + }); + }); +}); diff --git a/src/popup.ts b/src/popup.ts index 4b76401..e794c04 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -10,20 +10,6 @@ $(() => { return; } - $('#Setup').on('click', function(evt: Event) { - evt.preventDefault(); - chrome.runtime.sendMessage({ - type: 'setup', - }); - }); - - $('#Recovery').on('click', function(evt: Event) { - evt.preventDefault(); - chrome.runtime.sendMessage({ - type: 'recovery', - }); - }); - const tabKey = `tab-${currentTab.id}`; chrome.storage.local.get([tabKey], (result) => { log.debug('got storage results', result); diff --git a/webpack.common.js b/webpack.common.js index d023b14..00d2602 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -8,7 +8,8 @@ module.exports = { 'chromium/js/background.js': './src/background.ts', 'chromium/js/content_script.js': './src/content_script.ts', 'chromium/js/inject_webauthn.js': './src/inject_webauthn.ts', - 'chromium/js/popup.js': './src/popup.ts' + 'chromium/js/popup.js': './src/popup.ts', + 'chromium/js/option.js': './src/option.ts' }, output: { path: path.resolve(__dirname, 'dist'), From db6761626da395dab3a4ca158b963f7e8d784d88 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 6 Aug 2020 16:30:33 +0200 Subject: [PATCH 35/81] Add recovery and setup function to config page --- dist/chromium/options.html | 4 +-- .../styles/{option.css => options.css} | 0 src/background.ts | 10 ++++++- src/option.ts | 24 ---------------- src/options.ts | 28 +++++++++++++++++++ src/recovery.ts | 23 ++++++++++++--- webpack.common.js | 2 +- 7 files changed, 59 insertions(+), 32 deletions(-) rename dist/chromium/styles/{option.css => options.css} (100%) delete mode 100644 src/option.ts create mode 100644 src/options.ts diff --git a/dist/chromium/options.html b/dist/chromium/options.html index 40455c3..9f2c653 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -12,10 +12,10 @@ - + - + diff --git a/dist/chromium/styles/option.css b/dist/chromium/styles/options.css similarity index 100% rename from dist/chromium/styles/option.css rename to dist/chromium/styles/options.css diff --git a/src/background.ts b/src/background.ts index 2e98f0f..8184721 100644 --- a/src/background.ts +++ b/src/background.ts @@ -6,7 +6,7 @@ import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {processCredentialCreation, processCredentialRequest} from './webauthn'; -import {pskRecovery, pskSetup} from './recovery'; +import {pskRecovery, pskSetup, setBackupDeviceBaseUrl} from './recovery'; const log = getLogger('background'); @@ -48,6 +48,11 @@ const recovery = async () => { await pskRecovery(); }; +const saveOptions = async (msg) => { + log.debug('Save options called!'); + setBackupDeviceBaseUrl(msg); +}; + const create = async (msg, sender: chrome.runtime.MessageSender) => { if (!sender.tab || !sender.tab.id) { log.debug('received create event without a tab ID'); @@ -124,6 +129,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'recovery': recovery().then(() => alert('Recovery finished successfully!')); break; + case 'saveOptions': + saveOptions(msg.url).then(() => alert('Saving options successfully!')); + break; default: sendResponse(null); } diff --git a/src/option.ts b/src/option.ts deleted file mode 100644 index 34edec5..0000000 --- a/src/option.ts +++ /dev/null @@ -1,24 +0,0 @@ -import $ from 'jquery'; - -$(() => { - chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => { - const currentTab = tabs.find((t) => !!t.id); - if (!currentTab) { - return; - } - - $('#Setup').on('click', function(evt: Event) { - evt.preventDefault(); - chrome.runtime.sendMessage({ - type: 'setup', - }); - }); - - $('#Recovery').on('click', function(evt: Event) { - evt.preventDefault(); - chrome.runtime.sendMessage({ - type: 'recovery', - }); - }); - }); -}); diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..f53ff05 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,28 @@ +import $ from 'jquery'; +import {getBackupDeviceBaseUrl} from "./recovery"; + +$(() => { + $('#Setup').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'setup', + }); + }); + + $.when(getBackupDeviceBaseUrl()).then((url) => $('#BackupDeviceUrl').val(url)); + + $('#Recovery').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'recovery', + }); + }); + + $('#SaveBackupDeviceUrl').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'saveOptions', + url: $('#BackupDeviceUrl').val(), + }); + }); +}); diff --git a/src/recovery.ts b/src/recovery.ts index ef13fbf..10aa0c0 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -13,18 +13,31 @@ const log = getLogger('recovery'); export const PSK: string = 'psk'; -export const BACKUP_DEVICE_BASE_URL = 'http://localhost:8005'; // ToDo Load from a config file +const BACKUP_DEVICE_URL = 'bd_url'; + +export async function setBackupDeviceBaseUrl(url: string) { + await saveExportContainer(CONFIG, new Array(new ExportContainer(BACKUP_DEVICE_URL, url))); +} + +export async function getBackupDeviceBaseUrl(): Promise { + const ct = await fetchExportContainer(CONFIG).catch(_ => Array()); + const config = ct.filter((c) => c.id === BACKUP_DEVICE_URL); + return config.length !== 0 ? config[0].payload : 'http://localhost:8005'; +} export type ExportContainerType = string; const BACKUP: ExportContainerType = 'backup'; const RECOVERY: ExportContainerType = 'recovery'; const DELEGATION: ExportContainerType = 'delegation'; +const CONFIG: ExportContainerType = 'config'; export async function pskSetup() { const authId = prompt('Please enter a name for your authenticator', 'MyAuth'); const keyAmount: number = +prompt('How many backup keys should be created?', '5'); - await axios.default.post(BACKUP_DEVICE_BASE_URL + '/setup', {authId, keyAmount}) + const url = await getBackupDeviceBaseUrl(); + + await axios.default.post(url + '/setup', {authId, keyAmount}) .then(async function(response) { log.debug(response); const stpRsp = response.data; @@ -52,14 +65,16 @@ export async function pskSetup() { export async function pskRecovery() { const authId = prompt('Which authenticator you want to replace?', 'MyAuth'); - await axios.default.get(BACKUP_DEVICE_BASE_URL + '/recovery?authId=' + authId) + const url = await getBackupDeviceBaseUrl(); + + await axios.default.get(url + '/recovery?authId=' + authId) .then(async function(response1) { log.debug(response1); const keyAmount = response1.data.keyAmount; const rkData = await RecoveryKey.generate(keyAmount); - await axios.default.post(BACKUP_DEVICE_BASE_URL + '/recovery', {rkData, authId}) + await axios.default.post(url + '/recovery', {rkData, authId}) .then(async function(response2) { log.debug(response2); const rawDelegations = response2.data; diff --git a/webpack.common.js b/webpack.common.js index 00d2602..ea78baf 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -9,7 +9,7 @@ module.exports = { 'chromium/js/content_script.js': './src/content_script.ts', 'chromium/js/inject_webauthn.js': './src/inject_webauthn.ts', 'chromium/js/popup.js': './src/popup.ts', - 'chromium/js/option.js': './src/option.ts' + 'chromium/js/options.js': './src/options.ts' }, output: { path: path.resolve(__dirname, 'dist'), From 26c9e2bf41bff94d545a431fd743553b211e1d04 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Tue, 18 Aug 2020 19:26:35 +0200 Subject: [PATCH 36/81] Use PublicKeyCredentialSource --- src/recovery.ts | 9 +- src/storage.ts | 223 +++++++++++++++++++++++++++--------------------- src/webauthn.ts | 17 ++-- 3 files changed, 138 insertions(+), 111 deletions(-) diff --git a/src/recovery.ts b/src/recovery.ts index 10aa0c0..a4638f2 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -3,7 +3,7 @@ import * as CBOR from 'cbor'; import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin, padString} from './utils'; -import {fetchExportContainer, saveExportContainer, saveKey} from './storage'; +import {fetchExportContainer, saveExportContainer, PublicKeyCredentialSource} from './storage'; import {getCompatibleKeyFromCryptoKey} from './crypto'; @@ -351,7 +351,10 @@ export const recover = async ( log.debug('Recovery message', recMessage); const extOutput = await createPSKRecoveryExtensionOutput(recMessage); - await saveKey(encRkId, rkPrv.privateKey, pin); + const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); + + const publicKeyCredentialSource = new PublicKeyCredentialSource(encRkId, rkPrv.privateKey, rpID, null); + await publicKeyCredentialSource.store( pin); const clientData = await rkPrv.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, @@ -366,8 +369,6 @@ export const recover = async ( const clientDataJSON = base64ToByteArray(window.btoa(clientData)); const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), new Uint8Array(extOutput)); diff --git a/src/storage.ts b/src/storage.ts index 344b098..1d5df5e 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -8,24 +8,132 @@ import {ExportContainer, ExportContainerType} from './recovery'; const log = getLogger('storage'); -export const keyExists = (key: string): Promise => { - return new Promise(async (res, rej) => { - chrome.storage.sync.get({[key]: null}, (resp) => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - } else { - res(!(resp[key] == null)); - } +// https://www.w3.org/TR/webauthn/#public-key-credential-source +export class PublicKeyCredentialSource { + public static exits = (id: string): Promise => { + return new Promise(async (res, rej) => { + chrome.storage.sync.get({[id]: null}, (resp) => { + if (!!chrome.runtime.lastError) { + rej(chrome.runtime.lastError); + } else { + res(!(resp[id] == null)); + } + }); }); - }); -}; + }; -export const deleteKey = (key: string) => { - return new Promise(async (res, _) => { - chrome.storage.sync.remove(key); - res(); - }); -}; + public static async load(id: string, pin: string): Promise { + log.debug('Loading public key credential source for',id); + return new Promise(async (res, rej) => { + chrome.storage.sync.get({[id]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + rej(chrome.runtime.lastError); + return; + } + if (resp[id] == null) { + return rej('Public key credential source not found'); + } + + const json = JSON.parse(resp[id]); + + const _id = json.id; + const _rpId = json.rpId; + const _userHandle = json.userHandle; + + const keyPayload = base64ToByteArray(json.privateKey); + const saltByteLength = keyPayload[0]; + const ivByteLength = keyPayload[1]; + const keyAlgorithmByteLength = keyPayload[2]; + let offset = 3; + const salt = keyPayload.subarray(offset, offset + saltByteLength); + offset += saltByteLength; + const iv = keyPayload.subarray(offset, offset + ivByteLength); + offset += ivByteLength; + const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength); + offset += keyAlgorithmByteLength; + const keyBytes = keyPayload.subarray(offset); + + const wrappingKey = await getWrappingKey(pin, salt); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); + const _privateKey = await window.crypto.subtle.unwrapKey( + keyExportFormat, + keyBytes, + wrappingKey, + wrapAlgorithm, + unwrappingKeyAlgorithm, + true, + ['sign'], + ); + res(new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle)); + }); + }); + } + + public id: string + public privateKey: CryptoKey + public rpId: string + public userHandle: string + public type: string + + constructor(id: string, privateKey: CryptoKey, rpId: string, userHandle: string) { + this.id = id; + this.privateKey = privateKey; + this.rpId = rpId; + this.userHandle = userHandle; + this.type = "public-key"; + } + + public async store(pin: string): Promise { + return new Promise(async (res, rej) => { + if (!pin) { + rej('no pin provided'); + return; + } + const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); + const wrappingKey = await getWrappingKey(pin, salt); + const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + + const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( + keyExportFormat, + this.privateKey, + wrappingKey, + wrapAlgorithm, + ); + const wrappedKey = new Uint8Array(wrappedKeyBuffer); + const keyAlgorithm = new TextEncoder().encode(JSON.stringify(this.privateKey.algorithm)); + const payload = concatenate( + Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), + salt, + iv, + keyAlgorithm, + wrappedKey); + + const json = { + id: this.id, + privateKey: byteArrayToBase64(payload), + rpId: this.rpId, + userHandle: this.userHandle, + type: this.type + } + + chrome.storage.sync.set({[this.id]: JSON.stringify(json)}, () => { + if (!!chrome.runtime.lastError) { + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); + } +} const getWrappingKey = async (pin: string, salt: Uint8Array): Promise => { const enc = new TextEncoder(); @@ -91,86 +199,3 @@ export async function fetchExportContainer(cType: ExportContainerType): Promise< }); }); } - -export const fetchKey = async (key: string, pin: string): Promise => { - log.debug('Fetching key for', key); - return new Promise(async (res, rej) => { - chrome.storage.sync.get({[key]: null}, async (resp) => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - return; - } - if (resp[key] == null) { - return rej('Key not found'); - } - log.info('PIN', pin); - const payload = base64ToByteArray(resp[key]); - const saltByteLength = payload[0]; - const ivByteLength = payload[1]; - const keyAlgorithmByteLength = payload[2]; - let offset = 3; - const salt = payload.subarray(offset, offset + saltByteLength); - offset += saltByteLength; - const iv = payload.subarray(offset, offset + ivByteLength); - offset += ivByteLength; - const keyAlgorithmBytes = payload.subarray(offset, offset + keyAlgorithmByteLength); - offset += keyAlgorithmByteLength; - const keyBytes = payload.subarray(offset); - - const wrappingKey = await getWrappingKey(pin, salt); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); - window.crypto.subtle.unwrapKey( - keyExportFormat, - keyBytes, - wrappingKey, - wrapAlgorithm, - unwrappingKeyAlgorithm, - true, - ['sign'], - ).then(res, rej); - }); - }); -}; - -export const saveKey = (key: string, privateKey: CryptoKey, pin: string): Promise => { - return new Promise(async (res, rej) => { - if (!pin) { - rej('no pin provided'); - return; - } - const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(pin, salt); - const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - - const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( - keyExportFormat, - privateKey, - wrappingKey, - wrapAlgorithm, - ); - const wrappedKey = new Uint8Array(wrappedKeyBuffer); - const keyAlgorithm = new TextEncoder().encode(JSON.stringify(privateKey.algorithm)); - const payload = concatenate( - Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), - salt, - iv, - keyAlgorithm, - wrappedKey); - - chrome.storage.sync.set({[key]: byteArrayToBase64(payload)}, () => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - } else { - res(); - } - }); - }); -}; diff --git a/src/webauthn.ts b/src/webauthn.ts index 2c1ec3b..cdb05a1 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -4,7 +4,7 @@ import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; import {getLogger} from './logging'; -import {fetchKey, keyExists, saveKey} from './storage'; +import {PublicKeyCredentialSource} from './storage'; import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; @@ -41,7 +41,7 @@ export const processCredentialCreation = async ( const credId = base64ToByteArray(bckpKey.id, true); const encCredId = byteArrayToBase64(credId, true); - if (await keyExists(encCredId)) { + if (await PublicKeyCredentialSource.exits(encCredId)) { throw new Error(`credential with id ${encCredId} already exists`); } @@ -64,7 +64,8 @@ export const processCredentialCreation = async ( fmt: 'none', }).buffer; - await saveKey(encCredId, compatibleKey.privateKey, pin); + const publicKeyCredentialSource = new PublicKeyCredentialSource(encCredId, compatibleKey.privateKey, rpID, null) + await publicKeyCredentialSource.store(pin); log.debug('Attestation created'); @@ -100,7 +101,7 @@ export const processCredentialRequest = async ( } let i; - let key; + let publicKeyCredentialSource: PublicKeyCredentialSource; let credId: ArrayBuffer; let encCredId; for (i = 0; i < publicKeyRequestOptions.allowCredentials.length; i++) { @@ -108,19 +109,19 @@ export const processCredentialRequest = async ( credId = requestedCredential.id as ArrayBuffer; encCredId = byteArrayToBase64(new Uint8Array(credId), true); - key = await fetchKey(encCredId, pin).catch((_) => null); + publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); - if (key) { + if (publicKeyCredentialSource) { break; } } - if (!key) { + if (!publicKeyCredentialSource) { throw new Error(`credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)} not found`); } const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - const compatibleKey = await getCompatibleKeyFromCryptoKey(key); + const compatibleKey = await getCompatibleKeyFromCryptoKey(publicKeyCredentialSource.privateKey); const clientData = await compatibleKey.generateClientData( publicKeyRequestOptions.challenge as ArrayBuffer, { From 6e1d2f7a9dfcc81d8b299b1b9c34dc41b0e26e97 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 22 Aug 2020 10:06:18 +0200 Subject: [PATCH 37/81] Check excludeCredentials before generating new credential --- src/storage.ts | 4 ++-- src/webauthn.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/storage.ts b/src/storage.ts index 1d5df5e..74c1589 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -10,9 +10,9 @@ const log = getLogger('storage'); // https://www.w3.org/TR/webauthn/#public-key-credential-source export class PublicKeyCredentialSource { - public static exits = (id: string): Promise => { + public static async exits (id: string): Promise { return new Promise(async (res, rej) => { - chrome.storage.sync.get({[id]: null}, (resp) => { + chrome.storage.sync.get({[id]: null}, async (resp) => { if (!!chrome.runtime.lastError) { rej(chrome.runtime.lastError); } else { diff --git a/src/webauthn.ts b/src/webauthn.ts index cdb05a1..d5b1a2f 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -23,6 +23,19 @@ export const processCredentialCreation = async ( return null; } + let i; + for (i = 0; i < publicKeyCreationOptions.excludeCredentials.length; i++) { + const requestedCredential = publicKeyCreationOptions.excludeCredentials[i]; + const credId = requestedCredential.id as ArrayBuffer; + const encCredId = byteArrayToBase64(new Uint8Array(credId), true); + + const publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); + + if (publicKeyCredentialSource) { + throw new Error(`authenticator manages credential contained in excludeCredentials option.`); + } + } + let supportRecovery = false; const reqExt: any = publicKeyCreationOptions.extensions; if (reqExt !== undefined) { From 0a09dcd51ae13da846d3b8c880b8b5e999e2d82e Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 22 Aug 2020 11:43:27 +0200 Subject: [PATCH 38/81] Change PSK setup message structure --- src/background.ts | 3 ++- src/recovery.ts | 39 ++++++++++++++++++--------------------- src/webauthn.ts | 18 ++++++++++-------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/background.ts b/src/background.ts index 8184721..745f175 100644 --- a/src/background.ts +++ b/src/background.ts @@ -124,7 +124,8 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { } break; case 'setup': - setup().then(() => alert('Backup keys synchronized successfully!')); + setup().then(() => alert('Backup keys synchronized successfully!'), error => log.error('Setup' + + ' failed', error)) break; case 'recovery': recovery().then(() => alert('Recovery finished successfully!')); diff --git a/src/recovery.ts b/src/recovery.ts index a4638f2..38dfd34 100644 --- a/src/recovery.ts +++ b/src/recovery.ts @@ -31,24 +31,24 @@ const RECOVERY: ExportContainerType = 'recovery'; const DELEGATION: ExportContainerType = 'delegation'; const CONFIG: ExportContainerType = 'config'; -export async function pskSetup() { - const authId = prompt('Please enter a name for your authenticator', 'MyAuth'); +export async function pskSetup(): Promise { + const authAlias = prompt('Please enter a name for your authenticator', 'MyAuth'); const keyAmount: number = +prompt('How many backup keys should be created?', '5'); const url = await getBackupDeviceBaseUrl(); - await axios.default.post(url + '/setup', {authId, keyAmount}) + return await axios.default.post(url + '/setup', {authAlias, keyAmount}) .then(async function(response) { log.debug(response); const stpRsp = response.data; let i; const container = new Array(); for (i = 0; i < stpRsp.length; ++i) { - const jwk = stpRsp[i].jwk; + const jwk = stpRsp[i].publicBackupKey; const attObj = stpRsp[i].attObj; const parsedKey = await parseJWK(jwk, []); - const id = base64ToByteArray(jwk.kid, true); - const encId = byteArrayToBase64(id, true); + const credId = base64ToByteArray(stpRsp[i].credId, true); + const encId = byteArrayToBase64(credId, true); const bckpKey = new BackupKey(parsedKey, encId, attObj); const expBckpKey = await bckpKey.export(); container.push(expBckpKey); @@ -56,9 +56,6 @@ export async function pskSetup() { log.debug('Loaded backup keys', container); await saveExportContainer(BACKUP, container); - }) - .catch(function(error) { - log.error(error); }); } @@ -132,20 +129,20 @@ export class BackupKey { return await BackupKey.import(key); } - public key: CryptoKey; - public attObj: string; - public id: string; + public publicBackupKey: CryptoKey; + public backupDeviceAttObj: string; + public credentialId: string; constructor(key: CryptoKey, id: string, attObj: string) { - this.key = key; - this.id = id; - this.attObj = attObj; + this.publicBackupKey = key; + this.credentialId = id; + this.backupDeviceAttObj = attObj; } public async export(): Promise { - const jwk = await window.crypto.subtle.exportKey('jwk', this.key); - const rawJSON = {parsedKey: jwk, attObj: this.attObj}; - return new ExportContainer(this.id, JSON.stringify(rawJSON)); + const jwk = await window.crypto.subtle.exportKey('jwk', this.publicBackupKey); + const rawJSON = {parsedKey: jwk, attObj: this.backupDeviceAttObj}; + return new ExportContainer(this.credentialId, JSON.stringify(rawJSON)); } } @@ -172,14 +169,14 @@ export class RecoveryKey { const bckKey = await BackupKey.get(); const pubRk = await getCompatibleKeyFromCryptoKey(keyPair.publicKey); const pskSetup = await createPSKSetupExtensionOutput(bckKey); - const authData = await pubRk.generateAuthenticatorData('', 0, base64ToByteArray(bckKey.id, true), pskSetup); + const authData = await pubRk.generateAuthenticatorData('', 0, base64ToByteArray(bckKey.credentialId, true), pskSetup); const attObj = CBOR.encodeCanonical({ attStmt: new Map(), authData, fmt: 'none', }); - const exportRk = await (new RecoveryKey(bckKey.id, keyPair.privateKey, attObj)).export(); + const exportRk = await (new RecoveryKey(bckKey.credentialId, keyPair.privateKey, attObj)).export(); container.push(exportRk); delSetup.push(new ExportContainer(exportRk.id, padString(byteArrayToBase64(attObj, true)))); @@ -268,7 +265,7 @@ async function parseJWK(jwk, usages): Promise { } export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - const stpMsg = CBOR.encodeCanonical({attObj: base64ToByteArray(backupKey.attObj, true)}); + const stpMsg = CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.backupDeviceAttObj, true)}); const extOutput = new Map([[PSK, stpMsg]]); return new Uint8Array(CBOR.encodeCanonical(extOutput)); } diff --git a/src/webauthn.ts b/src/webauthn.ts index d5b1a2f..346ce00 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -24,15 +24,17 @@ export const processCredentialCreation = async ( } let i; - for (i = 0; i < publicKeyCreationOptions.excludeCredentials.length; i++) { - const requestedCredential = publicKeyCreationOptions.excludeCredentials[i]; - const credId = requestedCredential.id as ArrayBuffer; - const encCredId = byteArrayToBase64(new Uint8Array(credId), true); + if (publicKeyCreationOptions.excludeCredentials) { + for (i = 0; i < publicKeyCreationOptions.excludeCredentials.length; i++) { + const requestedCredential = publicKeyCreationOptions.excludeCredentials[i]; + const credId = requestedCredential.id as ArrayBuffer; + const encCredId = byteArrayToBase64(new Uint8Array(credId), true); - const publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); + const publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); - if (publicKeyCredentialSource) { - throw new Error(`authenticator manages credential contained in excludeCredentials option.`); + if (publicKeyCredentialSource) { + throw new Error(`authenticator manages credential contained in excludeCredentials option.`); + } } } @@ -51,7 +53,7 @@ export const processCredentialCreation = async ( const bckpKey = await BackupKey.get(); log.info('Use backup key', bckpKey); - const credId = base64ToByteArray(bckpKey.id, true); + const credId = base64ToByteArray(bckpKey.credentialId, true); const encCredId = byteArrayToBase64(credId, true); if (await PublicKeyCredentialSource.exits(encCredId)) { From 98a8338ac194f8eda92ddbf7d74c3e42b4f46d02 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 22 Aug 2020 21:22:48 +0200 Subject: [PATCH 39/81] Started separation of client and authenticator --- src/webauthn/authenticator/auth_storage.ts | 206 ++++++++++++++++++ .../authenticator/webauthn_authenticator.ts | 89 ++++++++ src/webauthn/authenticator/webauthn_crypto.ts | 53 +++++ src/webauthn/webauthn_client.ts | 19 ++ 4 files changed, 367 insertions(+) create mode 100644 src/webauthn/authenticator/auth_storage.ts create mode 100644 src/webauthn/authenticator/webauthn_authenticator.ts create mode 100644 src/webauthn/authenticator/webauthn_crypto.ts create mode 100644 src/webauthn/webauthn_client.ts diff --git a/src/webauthn/authenticator/auth_storage.ts b/src/webauthn/authenticator/auth_storage.ts new file mode 100644 index 0000000..8d10691 --- /dev/null +++ b/src/webauthn/authenticator/auth_storage.ts @@ -0,0 +1,206 @@ +import {base64ToByteArray, byteArrayToBase64, concatenate} from "../../utils"; +import {ivLength, keyExportFormat, saltLength} from "../../constants"; +import {getLogger} from "../../logging"; + +const log = getLogger('auth_storage'); +const PIN = "0000"; + +export class CredentialsMap { + public static async put(rpId: string, credSrc: PublicKeyCredentialSource): Promise { + log.debug(`Storing credential map entry for ${rpId}`); + const mapEntryExists = await this.exits(rpId); + let credSrcs: PublicKeyCredentialSource[]; + if (mapEntryExists) { + log.debug('Credential map entry does already exist. Update entry.'); + const entries = await this.load(rpId); + entries.push(credSrc); + credSrcs = entries; + } else { + log.debug('Credential map entry does not exist. Create new entry.'); + credSrcs = new Array(credSrc); + } + + // Store PublicKeyCredentialSource as JSON + let jsonArr = []; + for (let i = 0; i < credSrcs.length; i++) { + const json = await credSrcs[i].export(); + jsonArr.push(json); + } + let exportJSON = JSON.stringify(jsonArr); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[rpId]: exportJSON}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform CredentialsMap.put', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); + } + + public static async load(rpId: string): Promise { + log.debug(`Storing credential map entry for ${rpId}`); + return new Promise(async (res, rej) => { + chrome.storage.local.get({[rpId]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + rej(chrome.runtime.lastError); + return; + } + + if (resp[rpId] == null) { + log.warn(`CredentialsMap entry ${rpId} not found`); + res([]); + } + + const exportJSON = await JSON.parse(resp[rpId]); + const credSrcs = new Array(); + for (let i = 0; i < exportJSON.length; ++i) { + const credSrc = await PublicKeyCredentialSource.import(exportJSON[i]); + credSrcs.push(credSrc); + } + res(credSrcs); + }); + }); + } + + public static async lookup(rpId: string, credSrcId: string): Promise { + const credSrcs = await this.load(rpId); + const res = credSrcs.filter(x => x.id == credSrcId); + if (res.length == 0) { + return null; + } else { + return res[0]; + } + } + + public static async exits(rpId: string): Promise { + return new Promise(async (res, rej) => { + chrome.storage.sync.get({[rpId]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform CredentialsMap.exits', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + } else { + res(!(resp[rpId] == null)); + } + }); + }); + }; +} + +export class PublicKeyCredentialSource { + public static async import(json: any): Promise { + const _id = json.id; + const _rpId = json.rpId; + const _userHandle = json.userHandle; + + const keyPayload = base64ToByteArray(json.privateKey); + const saltByteLength = keyPayload[0]; + const ivByteLength = keyPayload[1]; + const keyAlgorithmByteLength = keyPayload[2]; + let offset = 3; + const salt = keyPayload.subarray(offset, offset + saltByteLength); + offset += saltByteLength; + const iv = keyPayload.subarray(offset, offset + ivByteLength); + offset += ivByteLength; + const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength); + offset += keyAlgorithmByteLength; + const keyBytes = keyPayload.subarray(offset); + + const wrappingKey = await getWrappingKey(PIN, salt); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); + const _privateKey = await window.crypto.subtle.unwrapKey( + keyExportFormat, + keyBytes, + wrappingKey, + wrapAlgorithm, + unwrappingKeyAlgorithm, + true, + ['sign'], + ); + log.debug('Deserialized PublicKeyCredentialSource with id', _id) + return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle); + } + + public id: string + public privateKey: CryptoKey + public rpId: string + public userHandle: string + public type: string + + constructor(id: string, privateKey: CryptoKey, rpId: string, userHandle?: string) { + this.id = id; + this.privateKey = privateKey; + this.rpId = rpId; + if (userHandle) { + this.userHandle = userHandle; + } else { + this.userHandle = ""; + } + this.type = "public-key"; + } + + public async export(): Promise { + const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); + const wrappingKey = await getWrappingKey(PIN, salt); + const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + + const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( + keyExportFormat, + this.privateKey, + wrappingKey, + wrapAlgorithm, + ); + const wrappedKey = new Uint8Array(wrappedKeyBuffer); + const keyAlgorithm = new TextEncoder().encode(JSON.stringify(this.privateKey.algorithm)); + const payload = concatenate( + Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), + salt, + iv, + keyAlgorithm, + wrappedKey); + + const json = { + id: this.id, + privateKey: byteArrayToBase64(payload), + rpId: this.rpId, + userHandle: this.userHandle, + type: this.type + } + + log.debug('Serialized PublicKeyCredentialSource with id', this.id) + return JSON.stringify(json); + } +} + +const getWrappingKey = async (pin: string, salt: Uint8Array): Promise => { + const enc = new TextEncoder(); + const derivationKey = await window.crypto.subtle.importKey( + 'raw', + enc.encode(pin), + {name: 'PBKDF2', length: 256}, + false, + ['deriveBits', 'deriveKey'], + ); + const pbkdf2Params: Pbkdf2Params = { + hash: 'SHA-256', + iterations: 100000, + name: 'PBKDF2', + salt, + }; + return window.crypto.subtle.deriveKey( + pbkdf2Params, + derivationKey, + {name: 'AES-GCM', length: 256}, + true, + ['wrapKey', 'unwrapKey'], + ); +}; \ No newline at end of file diff --git a/src/webauthn/authenticator/webauthn_authenticator.ts b/src/webauthn/authenticator/webauthn_authenticator.ts new file mode 100644 index 0000000..71e96ca --- /dev/null +++ b/src/webauthn/authenticator/webauthn_authenticator.ts @@ -0,0 +1,89 @@ +import {ECDSA, ES256_COSE} from "./webauthn_crypto"; +import {CredentialsMap, PublicKeyCredentialSource} from "./auth_storage"; +import {byteArrayToBase64} from "../../utils"; + +class Authenticator { + private static AAGUID: Uint8Array = new Uint8Array([ + 1214244733, 1205845608, 840015201, 3897052717, + 4072880437, 4027233456, 675224361, 2305433287, + 74291263, 3461796691, 701523034, 3178201666, + 3992003567, 1410532, 4234129691, 1438515639, + ]); + + private static getSignatureCounter(): number { + return 0; + } + + public static async authenticatorMakeCredential(hash: Uint8Array, + rpEntity: PublicKeyCredentialRpEntity, + userEntity: PublicKeyCredentialUserEntity, + requireResidentKey: boolean, + requireUserPresence: boolean, + requireUserVerification: boolean, + credTypesAndPubKeyAlgs: PublicKeyCredentialParameters[], + excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[], + extensions?: any): Promise { + // Step 2 + let algCheck = false; + for (let i = 0; i < credTypesAndPubKeyAlgs.length; i++) { + if (credTypesAndPubKeyAlgs[i].alg == ES256_COSE) { + algCheck = true; + break; + } + } + if (!algCheck) { + throw new Error(`authenticator does not support requested alg`); + } + + // Step 3 + if (excludeCredentialDescriptorList) { + const credMapEntries = await CredentialsMap.load(rpEntity.id); + for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { + const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; + const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); + if (credMapEntries.findIndex(x => x.id == credId) < 0) { + throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); + } + } + } + + // Step 6 + // ToDo User Consent + + // Step 5 + if (requireUserVerification) { + throw new Error(`authenticator does not support user verification`); + } + + // Step 7 + const credentialId = ""// ToDo Create id + const keyPair = await ECDSA.createECDSAKeyPair(); + const credSrc = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id) // No User Handle + await CredentialsMap.put(rpEntity.id, credSrc); + + // Step 9 + // ToDo Include Extension Processing + + // Step 10 + const sigCnt = this.getSignatureCounter(); + } + + private static async generateAttestedCredentialData(): Promise { + + // 16 bytes for the Authenticator Attestation GUID + authenticatorData.set(aaguid, offset); + offset += aaguid.length; + + // 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer. + authenticatorData.set(credIdLen, offset); + offset += credIdLen.length; + + // Variable length authenticator key ID + authenticatorData.set(credentialId, offset); + offset += credentialId.length; + + // Variable length public key + authenticatorData.set(encodedKey, offset); + offset += encodedKey.length; + } +} \ No newline at end of file diff --git a/src/webauthn/authenticator/webauthn_crypto.ts b/src/webauthn/authenticator/webauthn_crypto.ts new file mode 100644 index 0000000..2fe4f8f --- /dev/null +++ b/src/webauthn/authenticator/webauthn_crypto.ts @@ -0,0 +1,53 @@ +import {base64ToByteArray} from "../../utils"; + +export const ES256_COSE = -7 +export const ES256 = "P-256" +export const SHA256_COSE = 1 + +export interface ICOSECompatibleKey { + algorithm: number; + privateKey?: CryptoKey; + publicKey?: CryptoKey; + toCOSE(key: CryptoKey): Promise>; +} + +export class ECDSA implements ICOSECompatibleKey { + public algorithm: number + public privateKey: CryptoKey + public publicKey?: CryptoKey + + public static async createECDSAKeyPair(): Promise { + const keyPair = await window.crypto.subtle.generateKey( + { name: 'ECDSA', namedCurve: ES256 }, + true, + ['sign'], + ); + return new ECDSA(ES256_COSE, keyPair.privateKey, keyPair.publicKey); + } + + constructor( + algorithm: number, + privateKey: CryptoKey, + publicKey?: CryptoKey, + ) { + this.algorithm = algorithm; + this.privateKey = privateKey; + if (publicKey) { + this.publicKey = publicKey; + } + } + + public async toCOSE(key: CryptoKey): Promise> { + // In JWK the X and Y portions are Base64URL encoded (https://tools.ietf.org/html/rfc7517#section-3), + // which is just the right type for COSE encoding (https://tools.ietf.org/html/rfc8152#section-7), + // we just need to convert it to a byte array. + const exportedKey = await window.crypto.subtle.exportKey('jwk', key); + const attData = new Map(); + attData.set(1, 2); // EC2 key type + attData.set(3, this.algorithm); + attData.set(-1, SHA256_COSE); + attData.set(-2, base64ToByteArray(exportedKey.x, true)); + attData.set(-3, base64ToByteArray(exportedKey.y, true)); + return attData; + } +} \ No newline at end of file diff --git a/src/webauthn/webauthn_client.ts b/src/webauthn/webauthn_client.ts new file mode 100644 index 0000000..a35afc6 --- /dev/null +++ b/src/webauthn/webauthn_client.ts @@ -0,0 +1,19 @@ +import {byteArrayToBase64} from "../utils"; + +type FunctionType = string; +const Create: FunctionType = "webauthn.create" + +async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean): Promise { + const clientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); + + const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); + const clientDataHash = new Uint8Array(clientDataHashDigest); +} + +function generateClientDataJSON(type: FunctionType, challenge: ArrayBuffer, origin: string, tokenBinding?: string): any { + return { + type: type, + challenge: byteArrayToBase64(Buffer.from(challenge), true), + origin: origin, + } +} \ No newline at end of file From 930a75c7cb7f9a6ed75a064d230cee11e710878f Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 23 Aug 2020 15:37:05 +0200 Subject: [PATCH 40/81] Finished registration flow --- package-lock.json | 55 ++++- package.json | 3 + src/background.ts | 7 +- src/constants.ts | 4 + src/utils.ts | 24 +++ .../auth_storage.ts => webauth_storage.ts} | 18 +- .../authenticator/webauthn_authenticator.ts | 89 -------- src/webauthn/webauthn_client.ts | 19 -- src/webauthn_authenticator.ts | 201 ++++++++++++++++++ src/webauthn_client.ts | 54 +++++ .../authenticator => }/webauthn_crypto.ts | 55 ++++- 11 files changed, 403 insertions(+), 126 deletions(-) rename src/{webauthn/authenticator/auth_storage.ts => webauth_storage.ts} (92%) delete mode 100644 src/webauthn/authenticator/webauthn_authenticator.ts delete mode 100644 src/webauthn/webauthn_client.ts create mode 100644 src/webauthn_authenticator.ts create mode 100644 src/webauthn_client.ts rename src/{webauthn/authenticator => }/webauthn_crypto.ts (50%) diff --git a/package-lock.json b/package-lock.json index 0ae85f3..0600921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -317,6 +317,27 @@ } } }, + "@fidm/asn1": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@fidm/asn1/-/asn1-1.0.4.tgz", + "integrity": "sha512-esd1jyNvRb2HVaQGq2Gg8Z0kbQPXzV9Tq5Z14KNIov6KfFD6PTaRIO8UpcsYiTNzOqJpmyzWgVTrUwFV3UF4TQ==" + }, + "@fidm/x509": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@fidm/x509/-/x509-1.2.1.tgz", + "integrity": "sha512-nwc2iesjyc9hkuzcrMCBXQRn653XuAUKorfWM8PZyJawiy1QzLj4vahwzaI25+pfpwOLvMzbJ0uKpWLDNmo16w==", + "requires": { + "@fidm/asn1": "^1.0.4", + "tweetnacl": "^1.0.1" + }, + "dependencies": { + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + } + } + }, "@jest/console": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", @@ -3304,6 +3325,32 @@ "stream-shift": "^1.0.0" } }, + "ec-key": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/ec-key/-/ec-key-0.0.4.tgz", + "integrity": "sha512-jJkzm7XWb4Bcu4TpuQnOZD/OGSW3Y35S0Qe0cpnJsricaJtC+Tl3I0OKwgQasxNiEFt0Czv89ncq7vrHSgMj/w==", + "requires": { + "asn1.js": "^5.2.0" + }, + "dependencies": { + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, + "bn.js": { + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", + "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==" + } + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -7403,6 +7450,11 @@ "sha.js": "^2.4.8" } }, + "pem-file": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pem-file/-/pem-file-1.0.1.tgz", + "integrity": "sha512-jIDhaSc4Pk8go+kDYJJ2aS7Bg8Lxvir02NnGp9B1bdJpKiDH680ULl+Duh0jBkz8gV3PywEAWz9XNYqLcd6kVg==" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -8060,8 +8112,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sane": { "version": "4.1.0", diff --git a/package.json b/package.json index d3e6f38..5f6e13c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "zip-folder": "^1.0.0" }, "dependencies": { + "@fidm/x509": "^1.2.1", "@types/chrome": "^0.0.77", "@types/jquery": "^3.3.31", "@types/loglevel": "^1.6.3", @@ -33,11 +34,13 @@ "axios": "^0.19.2", "bn.js": "^5.1.2", "cbor": "^4.3.0", + "ec-key": "0.0.4", "elliptic": "^6.5.3", "jquery": "^3.5.1", "jsrsasign": "^8.0.20", "loglevel": "^1.6.6", "loglevel-plugin-prefix": "^0.8.4", + "pem-file": "^1.0.1", "strip-sourcemap-loader": "^0.0.1", "web-ext-types": "^3.2.1" }, diff --git a/src/background.ts b/src/background.ts index 745f175..57284fd 100644 --- a/src/background.ts +++ b/src/background.ts @@ -7,6 +7,7 @@ import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {processCredentialCreation, processCredentialRequest} from './webauthn'; import {pskRecovery, pskSetup, setBackupDeviceBaseUrl} from './recovery'; +import {createPublicKeyCredential} from "./webauthn_client"; const log = getLogger('background'); @@ -64,10 +65,10 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { try { const opts = webauthnParse(msg.options); - const credential = await processCredentialCreation( + const credential = await createPublicKeyCredential( origin, - opts.publicKey, - `${pin}`, + opts, + false, ); return { credential: webauthnStringify(credential), diff --git a/src/constants.ts b/src/constants.ts index aaa403d..fc7015a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,3 +15,7 @@ export const enabledIcons = { 48: 'images/lock_enabled-48.png', 128: 'images/lock_enabled-128.png', }; + +export const ES256_COSE = -7 +export const ES256 = "P-256" +export const SHA256_COSE = 1 diff --git a/src/utils.ts b/src/utils.ts index 7c1eee5..c796ec3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -39,6 +39,30 @@ export function concatenate(...arrays: Uint8Array[]) { return result; } +export function counterToBytes(c: number): Uint8Array { + const bytes = new Uint8Array(4); + // Sadly, JS TypedArrays are whatever-endian the platform is, + // so Uint32Array is not at all useful here (or anywhere?), + // and we must manually pack the counter (big endian as per spec). + bytes[0] = 0xFF & c >>> 24; + bytes[1] = 0xFF & c >>> 16; + bytes[2] = 0xFF & c >>> 8; + bytes[3] = 0xFF & c; + return bytes; +} + +/*export function pemToArrayBuffer(pem) { + var b64Lines = removeLines(pem); + var b64Prefix = b64Lines.replace('-----BEGIN PRIVATE KEY-----', ''); + var b64Final = b64Prefix.replace('-----END PRIVATE KEY-----', ''); + + return base64ToArrayBuffer(b64Final); +} + +function removeLines(str) { + return str.replace("\n", ""); +}*/ + // Copyright 2014 Google Inc. All rights reserved // // Use of this source code is governed by a BSD-style diff --git a/src/webauthn/authenticator/auth_storage.ts b/src/webauth_storage.ts similarity index 92% rename from src/webauthn/authenticator/auth_storage.ts rename to src/webauth_storage.ts index 8d10691..6d3ea67 100644 --- a/src/webauthn/authenticator/auth_storage.ts +++ b/src/webauth_storage.ts @@ -1,6 +1,6 @@ -import {base64ToByteArray, byteArrayToBase64, concatenate} from "../../utils"; -import {ivLength, keyExportFormat, saltLength} from "../../constants"; -import {getLogger} from "../../logging"; +import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; +import {ivLength, keyExportFormat, saltLength} from "./constants"; +import {getLogger} from "./logging"; const log = getLogger('auth_storage'); const PIN = "0000"; @@ -40,7 +40,7 @@ export class CredentialsMap { } public static async load(rpId: string): Promise { - log.debug(`Storing credential map entry for ${rpId}`); + log.debug(`Loading credential map entry for ${rpId}`); return new Promise(async (res, rej) => { chrome.storage.local.get({[rpId]: null}, async (resp) => { if (!!chrome.runtime.lastError) { @@ -59,6 +59,7 @@ export class CredentialsMap { const credSrc = await PublicKeyCredentialSource.import(exportJSON[i]); credSrcs.push(credSrc); } + log.debug('Loaded credential map entry successfully'); res(credSrcs); }); }); @@ -76,7 +77,7 @@ export class CredentialsMap { public static async exits(rpId: string): Promise { return new Promise(async (res, rej) => { - chrome.storage.sync.get({[rpId]: null}, async (resp) => { + chrome.storage.local.get({[rpId]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.error('Could not perform CredentialsMap.exits', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); @@ -90,6 +91,7 @@ export class CredentialsMap { export class PublicKeyCredentialSource { public static async import(json: any): Promise { + log.debug('Import PublicKeyCredentialSource', json); const _id = json.id; const _rpId = json.rpId; const _userHandle = json.userHandle; @@ -122,7 +124,7 @@ export class PublicKeyCredentialSource { true, ['sign'], ); - log.debug('Deserialized PublicKeyCredentialSource with id', _id) + log.debug('Imported PublicKeyCredentialSource with id', _id) return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle); } @@ -176,8 +178,8 @@ export class PublicKeyCredentialSource { type: this.type } - log.debug('Serialized PublicKeyCredentialSource with id', this.id) - return JSON.stringify(json); + log.debug('Exported PublicKeyCredentialSource with id', this.id) + return json; } } diff --git a/src/webauthn/authenticator/webauthn_authenticator.ts b/src/webauthn/authenticator/webauthn_authenticator.ts deleted file mode 100644 index 71e96ca..0000000 --- a/src/webauthn/authenticator/webauthn_authenticator.ts +++ /dev/null @@ -1,89 +0,0 @@ -import {ECDSA, ES256_COSE} from "./webauthn_crypto"; -import {CredentialsMap, PublicKeyCredentialSource} from "./auth_storage"; -import {byteArrayToBase64} from "../../utils"; - -class Authenticator { - private static AAGUID: Uint8Array = new Uint8Array([ - 1214244733, 1205845608, 840015201, 3897052717, - 4072880437, 4027233456, 675224361, 2305433287, - 74291263, 3461796691, 701523034, 3178201666, - 3992003567, 1410532, 4234129691, 1438515639, - ]); - - private static getSignatureCounter(): number { - return 0; - } - - public static async authenticatorMakeCredential(hash: Uint8Array, - rpEntity: PublicKeyCredentialRpEntity, - userEntity: PublicKeyCredentialUserEntity, - requireResidentKey: boolean, - requireUserPresence: boolean, - requireUserVerification: boolean, - credTypesAndPubKeyAlgs: PublicKeyCredentialParameters[], - excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[], - extensions?: any): Promise { - // Step 2 - let algCheck = false; - for (let i = 0; i < credTypesAndPubKeyAlgs.length; i++) { - if (credTypesAndPubKeyAlgs[i].alg == ES256_COSE) { - algCheck = true; - break; - } - } - if (!algCheck) { - throw new Error(`authenticator does not support requested alg`); - } - - // Step 3 - if (excludeCredentialDescriptorList) { - const credMapEntries = await CredentialsMap.load(rpEntity.id); - for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { - const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; - const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - if (credMapEntries.findIndex(x => x.id == credId) < 0) { - throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); - } - } - } - - // Step 6 - // ToDo User Consent - - // Step 5 - if (requireUserVerification) { - throw new Error(`authenticator does not support user verification`); - } - - // Step 7 - const credentialId = ""// ToDo Create id - const keyPair = await ECDSA.createECDSAKeyPair(); - const credSrc = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id) // No User Handle - await CredentialsMap.put(rpEntity.id, credSrc); - - // Step 9 - // ToDo Include Extension Processing - - // Step 10 - const sigCnt = this.getSignatureCounter(); - } - - private static async generateAttestedCredentialData(): Promise { - - // 16 bytes for the Authenticator Attestation GUID - authenticatorData.set(aaguid, offset); - offset += aaguid.length; - - // 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer. - authenticatorData.set(credIdLen, offset); - offset += credIdLen.length; - - // Variable length authenticator key ID - authenticatorData.set(credentialId, offset); - offset += credentialId.length; - - // Variable length public key - authenticatorData.set(encodedKey, offset); - offset += encodedKey.length; - } -} \ No newline at end of file diff --git a/src/webauthn/webauthn_client.ts b/src/webauthn/webauthn_client.ts deleted file mode 100644 index a35afc6..0000000 --- a/src/webauthn/webauthn_client.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {byteArrayToBase64} from "../utils"; - -type FunctionType = string; -const Create: FunctionType = "webauthn.create" - -async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean): Promise { - const clientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); - - const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); - const clientDataHash = new Uint8Array(clientDataHashDigest); -} - -function generateClientDataJSON(type: FunctionType, challenge: ArrayBuffer, origin: string, tokenBinding?: string): any { - return { - type: type, - challenge: byteArrayToBase64(Buffer.from(challenge), true), - origin: origin, - } -} \ No newline at end of file diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts new file mode 100644 index 0000000..4878189 --- /dev/null +++ b/src/webauthn_authenticator.ts @@ -0,0 +1,201 @@ +import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; +import {CredentialsMap, PublicKeyCredentialSource} from "./webauth_storage"; +import {base64ToByteArray, byteArrayToBase64, counterToBytes} from "./utils"; +import * as CBOR from 'cbor'; +import {createAttestationSignature, getAttestationCertificate} from "./webauthn_attestation"; +import {getLogger} from "./logging"; +import {ES256_COSE} from "./constants"; + +const log = getLogger('webauthn_authenticator'); + +export class AttestationObjectWrapper { + public credentialId: string + public rawAttObj: Uint8Array + + constructor(credId: string, raw: Uint8Array) { + this.credentialId = credId; + this.rawAttObj = raw; + } +} + +export class Authenticator { + private static AAGUID: Uint8Array = new Uint8Array([ + 1214244733, 1205845608, 840015201, 3897052717, + 4072880437, 4027233456, 675224361, 2305433287, + 74291263, 3461796691, 701523034, 3178201666, + 3992003567, 1410532, 4234129691, 1438515639, + ]); + + private static getSignatureCounter(): number { + return 0; + } + + public static async authenticatorMakeCredential(hash: Uint8Array, + rpEntity: PublicKeyCredentialRpEntity, + userEntity: PublicKeyCredentialUserEntity, + requireResidentKey: boolean, + requireUserPresence: boolean, + requireUserVerification: boolean, + credTypesAndPubKeyAlgs: PublicKeyCredentialParameters[], + excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[], + extensions?: any): Promise { + log.debug('Called authenticatorMakeCredential'); + + // Step 2 + let algCheck = false; + for (let i = 0; i < credTypesAndPubKeyAlgs.length; i++) { + if (credTypesAndPubKeyAlgs[i].alg == ES256_COSE) { + algCheck = true; + break; + } + } + if (!algCheck) { + throw new Error(`authenticator does not support requested alg`); + } + + // Step 3 + if (excludeCredentialDescriptorList) { + const credMapEntries = await CredentialsMap.load(rpEntity.id); + for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { + const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; + const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); + if (credMapEntries.findIndex(x => x.id == credId) >= 0) { + throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); + } + } + } + + // Step 6 + // ToDo User Consent + + // Step 5 + if (requireUserVerification) { + throw new Error(`authenticator does not support user verification`); + } + + // Step 7 + const credentialId = this.createCredentialId() + const keyPair = await ECDSA.createECDSAKeyPair(); + const credSrc = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id) // No User Handle + await CredentialsMap.put(rpEntity.id, credSrc); + + // Step 9 + // ToDo Include Extension Processing + const extensionData = undefined; + + // Step 10 + const sigCnt = this.getSignatureCounter(); + + // Step 11 + const rawCredentialId = base64ToByteArray(credentialId, true); + const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); + + // Step 12 + const authenticatorData = await this.generateAuthenticatorData(rpEntity.id, sigCnt, attestedCredentialData, extensionData); + + // Step 13 + const attObj = await this.generateAttestationObject(hash, authenticatorData); + + // Return value is not 1:1 WebAuthn conform + log.debug('Created credential', credentialId) + return (new AttestationObjectWrapper(credentialId, attObj)); + + } + + private static async generateAttestedCredentialData(credentialId: Uint8Array, keyPair: ICOSECompatibleKey): Promise { + const aaguid = this.AAGUID.slice(0, 16); + const credIdLen = new Uint8Array(2); + credIdLen[0] = (credentialId.length >> 8) & 0xff; + credIdLen[1] = credentialId.length & 0xff; + const coseKey = await keyPair.toCOSE(keyPair.publicKey); + const encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); + + const attestedCredentialDataLength = aaguid.length + credIdLen.length + credentialId.length + encodedKey.length; + const attestedCredentialData = new Uint8Array(attestedCredentialDataLength); + + let offset = 0; + attestedCredentialData.set(aaguid, offset); + offset += aaguid.length; + + attestedCredentialData.set(credIdLen, offset); + offset += credIdLen.length; + + attestedCredentialData.set(credentialId, offset); + offset += credentialId.length; + + attestedCredentialData.set(encodedKey, offset); + + return attestedCredentialData; + } + + private static async generateAuthenticatorData(rpID: string, counter: number, attestedCredentialData?: Uint8Array, + extensionData?: Uint8Array): Promise { + const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); + const rpIdHash = new Uint8Array(rpIdDigest); + let authenticatorDataLength = rpIdHash.length + 1 + 4; + if (attestedCredentialData) { + authenticatorDataLength += attestedCredentialData.byteLength; + } + if (extensionData) { + authenticatorDataLength += extensionData.byteLength; + } + + const authenticatorData = new Uint8Array(authenticatorDataLength); + let offset = 0; + + // 32 bytes for the RP ID hash + authenticatorData.set(rpIdHash, offset); + offset += rpIdHash.length; + + // 1 byte for flags + authenticatorData[rpIdHash.length] = 1; // UP + if (attestedCredentialData) { + authenticatorData[rpIdHash.length] |= (1 << 6); // AT + } + if (extensionData) { + authenticatorData[rpIdHash.length] |= (1 << 7); // ED + } + offset++; + + // 4 bytes for the counter. big-endian uint32 + // https://www.w3.org/TR/webauthn/#signature-counter + authenticatorData.set(counterToBytes(counter), offset); + offset += counterToBytes(counter).length; + + if (attestedCredentialData) { + authenticatorData.set(attestedCredentialData, offset); + offset += attestedCredentialData.byteLength; + } + if (extensionData) { + authenticatorData.set(extensionData, offset); + } + return authenticatorData; + } + + private static async generateAttestationObject(hash: Uint8Array, authenticatorData: Uint8Array): Promise { + const attCert = getAttestationCertificate(); + const attSignature = await createAttestationSignature(hash, authenticatorData); + const attObjJSON = { + authData: authenticatorData, + fmt: 'packed', + attStmt: { + alg: ES256_COSE, + sig: attSignature, + x5c: [attCert] + } + } + return CBOR.encodeCanonical(attObjJSON); + + } + + private static createCredentialId(): string{ + let enc = new TextEncoder(); + let dt = new Date().getTime(); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = (dt + Math.random()*16)%16 | 0; + dt = Math.floor(dt/16); + return (c=='x' ? r :(r&0x3|0x8)).toString(16); + }); + return byteArrayToBase64(enc.encode(uuid), true); + } +} \ No newline at end of file diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts new file mode 100644 index 0000000..4e7a677 --- /dev/null +++ b/src/webauthn_client.ts @@ -0,0 +1,54 @@ +import {base64ToByteArray, byteArrayToBase64} from "./utils"; +import {Authenticator} from "./webauthn_authenticator"; +import {getLogger} from "./logging"; + +type FunctionType = string; +const Create: FunctionType = "webauthn.create" + +const log = getLogger('webauthn_authenticator'); + +export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean): Promise { + log.debug('Called createPublicKeyCredential'); + if (options.publicKey.attestation) { + if (options.publicKey.attestation !== "direct") { + throw new Error(`Only direct attestation supported`); + } + } + + const clientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); + + const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); + const clientDataHash = new Uint8Array(clientDataHashDigest); + + const requireUserVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required" + + const attObjWrapper = await Authenticator.authenticatorMakeCredential(clientDataHash, + options.publicKey.rp, + options.publicKey.user, + options.publicKey.authenticatorSelection.requireResidentKey, + !requireUserVerification, + requireUserVerification, + options.publicKey.pubKeyCredParams, + options.publicKey.excludeCredentials) // ToDo Add extensions map + + log.debug('Received attestation object'); + + return { + getClientExtensionResults: () => ({}), + id: attObjWrapper.credentialId, + rawId: base64ToByteArray(attObjWrapper.credentialId, true), + response: { + attestationObject: attObjWrapper.rawAttObj.buffer, + clientDataJSON: base64ToByteArray(window.btoa(JSON.stringify(clientDataJSON))), + }, + type: 'public-key', + } as PublicKeyCredential; +} + +function generateClientDataJSON(type: FunctionType, challenge: ArrayBuffer, origin: string, tokenBinding?: string): any { + return { + type: type, + challenge: byteArrayToBase64(Buffer.from(challenge), true), + origin: origin, + } +} \ No newline at end of file diff --git a/src/webauthn/authenticator/webauthn_crypto.ts b/src/webauthn_crypto.ts similarity index 50% rename from src/webauthn/authenticator/webauthn_crypto.ts rename to src/webauthn_crypto.ts index 2fe4f8f..992996b 100644 --- a/src/webauthn/authenticator/webauthn_crypto.ts +++ b/src/webauthn_crypto.ts @@ -1,14 +1,14 @@ -import {base64ToByteArray} from "../../utils"; - -export const ES256_COSE = -7 -export const ES256 = "P-256" -export const SHA256_COSE = 1 +import * as asn1 from 'asn1.js'; +import {BN} from 'bn.js'; +import {base64ToByteArray} from "./utils"; +import {ES256, ES256_COSE, SHA256_COSE} from "./constants"; export interface ICOSECompatibleKey { algorithm: number; privateKey?: CryptoKey; publicKey?: CryptoKey; toCOSE(key: CryptoKey): Promise>; + sign(data: Uint8Array): Promise } export class ECDSA implements ICOSECompatibleKey { @@ -16,6 +16,14 @@ export class ECDSA implements ICOSECompatibleKey { public privateKey: CryptoKey public publicKey?: CryptoKey + public static async fromKey(key: CryptoKey): Promise { + if (key.type === 'public') { + return new ECDSA(ES256_COSE, null, key); + } else { + return new ECDSA(ES256_COSE, key); + } + } + public static async createECDSAKeyPair(): Promise { const keyPair = await window.crypto.subtle.generateKey( { name: 'ECDSA', namedCurve: ES256 }, @@ -50,4 +58,41 @@ export class ECDSA implements ICOSECompatibleKey { attData.set(-3, base64ToByteArray(exportedKey.y, true)); return attData; } + + public async sign(data: Uint8Array): Promise { + if (!this.privateKey) { + throw new Error('no private key available for signing'); + } + const rawSign = await window.crypto.subtle.sign( // Creates digest Hash before signing + { name: 'ECDSA', hash: 'SHA-256' }, + this.privateKey, + data, + ); + + const rawSignBuf = new Buffer(rawSign); + + // Credit to: https://stackoverflow.com/a/39651457/5333936 + const ecdsaDerSig = asn1.define('ECPrivateKey', function() { + return this.seq().obj( + this.key('r').int(), + this.key('s').int(), + ); + }); + const r = new BN(rawSignBuf.slice(0, 32).toString('hex'), 16, 'be'); + const s = new BN(rawSignBuf.slice(32).toString('hex'), 16, 'be'); + return new Uint8Array(ecdsaDerSig.encode({r, s}, 'der')); + } +} + +export async function importFromJWK(jwk, usages): Promise { + return window.crypto.subtle.importKey( + 'jwk', + jwk, + { + name: 'ECDSA', + namedCurve: ES256, + }, + true, + usages, + ); } \ No newline at end of file From 3cfe685c0c40dbf975e35cf83645abae3254e93e Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 23 Aug 2020 15:37:53 +0200 Subject: [PATCH 41/81] Add attestation --- src/webauthn_attestation.ts | 51 +++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/webauthn_attestation.ts diff --git a/src/webauthn_attestation.ts b/src/webauthn_attestation.ts new file mode 100644 index 0000000..222052b --- /dev/null +++ b/src/webauthn_attestation.ts @@ -0,0 +1,51 @@ +import {ECDSA, ICOSECompatibleKey, importFromJWK} from "./webauthn_crypto"; + +const PrivateKeyPEM = '-----BEGIN EC PRIVATE KEY-----\n' + + 'MHcCAQEEIOOF5RiIjzKZCCtFJLMxAFB4O8WvnhAsWuF9YacETrWgoAoGCCqGSM49\n' + + 'AwEHoUQDQgAEaixHolNWEXlB7JdX+2WgUeM3BfbvLPsVaNKe+Efu4ea3iMBvNelS\n' + + '5jOgQ2DYpKv6FHtoUhT6rBpzYmc/pgd6XA==\n' + + '-----END EC PRIVATE KEY-----'; + +const AttestationCertificatePEM = '-----BEGIN CERTIFICATE-----\n' + + 'MIIDLzCCAtWgAwIBAgIJAOe4D4tkNjKBMAoGCCqGSM49BAMCMIGwMQswCQYDVQQG\n' + + 'EwJERTEPMA0GA1UECAwGU2F4b255MRAwDgYDVQQHDAdEcmVzZGVuMRowGAYDVQQK\n' + + 'DBFBdXRoIEV4YW1wbGUsIExMQzEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRl\n' + + 'c3RhdGlvbjEdMBsGA1UEAwwUQXV0aCBFeGFtcGxlIENvbXBhbnkxHzAdBgkqhkiG\n' + + '9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMjAwODIzMDkzODM1WhcNMjEwODIz\n' + + 'MDkzODM1WjCBsDELMAkGA1UEBhMCREUxDzANBgNVBAgMBlNheG9ueTEQMA4GA1UE\n' + + 'BwwHRHJlc2RlbjEaMBgGA1UECgwRQXV0aCBFeGFtcGxlLCBMTEMxIjAgBgNVBAsM\n' + + 'GUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xHTAbBgNVBAMMFEF1dGggRXhhbXBs\n' + + 'ZSBDb21wYW55MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMFkwEwYH\n' + + 'KoZIzj0CAQYIKoZIzj0DAQcDQgAEaixHolNWEXlB7JdX+2WgUeM3BfbvLPsVaNKe\n' + + '+Efu4ea3iMBvNelS5jOgQ2DYpKv6FHtoUhT6rBpzYmc/pgd6XKOB1TCB0jAdBgNV\n' + + 'HQ4EFgQU6EhpfP6IdPer5CCYCIsbDOexo2YwHwYDVR0jBBgwFoAU6EhpfP6IdPer\n' + + '5CCYCIsbDOexo2YwCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwSgYDVR0RBEMwQYIL\n' + + 'ZXhhbXBsZS5jb22CD3d3dy5leGFtcGxlLmNvbYIQbWFpbC5leGFtcGxlLmNvbYIP\n' + + 'ZnRwLmV4YW1wbGUuY29tMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRl\n' + + 'ZCBDZXJ0aWZpY2F0ZTAKBggqhkjOPQQDAgNIADBFAiEA4Rwn4jcj50HYQ5N6UJaT\n' + + 'UxuwhZgl5yLEJOzvY3a2V/gCIFwJNEMUE0PeRrhUoEWmj1zg2kV8EEzHO1bio6q0\n' + + 'o9rQ\n' + + '-----END CERTIFICATE-----'; + +export async function createAttestationSignature(hash: Uint8Array, authData: Uint8Array): Promise { + const attPrvKey = await getAttestationPrivateKey(); + const concatData = new Uint8Array(authData.length + hash.length); + concatData.set(authData); + concatData.set(hash, authData.length); + return await attPrvKey.sign(concatData); +} + +async function getAttestationPrivateKey(): Promise { + const ECKey = require('ec-key'); + const prvKey = new ECKey(Buffer.from(PrivateKeyPEM), 'pem'); + const jwk = JSON.stringify(prvKey, null, 2); + const key = await importFromJWK(JSON.parse(jwk), ['sign']); + return ECDSA.fromKey(key); +} + +export function getAttestationCertificate(): Uint8Array { + const pem = require('pem-file') + const decPem = pem.decode(Buffer.from(AttestationCertificatePEM)); + return decPem; +} + From b8222591b2fc0e60b4064c6eb22bc0752de2086d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 24 Aug 2020 17:13:14 +0200 Subject: [PATCH 42/81] Add assertion flow --- src/background.ts | 20 +++------ src/webauth_storage.ts | 6 +-- src/webauthn_attestation.ts | 3 +- src/webauthn_authenticator.ts | 80 ++++++++++++++++++++++++++++++++--- src/webauthn_client.ts | 75 +++++++++++++++++++++++++++----- 5 files changed, 147 insertions(+), 37 deletions(-) diff --git a/src/background.ts b/src/background.ts index 57284fd..4248fbd 100644 --- a/src/background.ts +++ b/src/background.ts @@ -7,7 +7,7 @@ import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {processCredentialCreation, processCredentialRequest} from './webauthn'; import {pskRecovery, pskSetup, setBackupDeviceBaseUrl} from './recovery'; -import {createPublicKeyCredential} from "./webauthn_client"; +import {createPublicKeyCredential, getPublicKeyCredential} from "./webauthn_client"; const log = getLogger('background'); @@ -68,7 +68,7 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { const credential = await createPublicKeyCredential( origin, opts, - false, + true, ); return { credential: webauthnStringify(credential), @@ -76,12 +76,7 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => { type: 'create_response', }; } catch (e) { - if (e instanceof DOMException) { - const { code, message, name } = e; - log.error('failed to import key due to DOMException', { code, message, name }, e); - } else { - log.error('failed to import key', { errorType: `${(typeof e)}` }, e); - } + log.error('failed to register credential', { errorType: `${(typeof e)}` }, e); } }; @@ -91,19 +86,14 @@ const sign = async (msg, sender: chrome.runtime.MessageSender) => { const pin = await requestPin(sender.tab.id, origin); try { - const credential = await processCredentialRequest(origin, opts.publicKey, `${pin}`); + const credential = await getPublicKeyCredential(origin, opts, true); return { credential: webauthnStringify(credential), requestID: msg.requestID, type: 'sign_response', }; } catch (e) { - if (e instanceof DOMException) { - const { code, message, name } = e; - log.error('failed to sign due DOMException', { code, message, name }, e); - } else { - log.error('failed to sign', { errorType: `${(typeof e)}` }, e); - } + log.error('failed to create assertion', { errorType: `${(typeof e)}` }, e); } }; diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 6d3ea67..7616e6c 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -131,17 +131,17 @@ export class PublicKeyCredentialSource { public id: string public privateKey: CryptoKey public rpId: string - public userHandle: string + public userHandle: Uint8Array public type: string - constructor(id: string, privateKey: CryptoKey, rpId: string, userHandle?: string) { + constructor(id: string, privateKey: CryptoKey, rpId: string, userHandle?: Uint8Array) { this.id = id; this.privateKey = privateKey; this.rpId = rpId; if (userHandle) { this.userHandle = userHandle; } else { - this.userHandle = ""; + this.userHandle = null; } this.type = "public-key"; } diff --git a/src/webauthn_attestation.ts b/src/webauthn_attestation.ts index 222052b..729bc20 100644 --- a/src/webauthn_attestation.ts +++ b/src/webauthn_attestation.ts @@ -45,7 +45,6 @@ async function getAttestationPrivateKey(): Promise { export function getAttestationCertificate(): Uint8Array { const pem = require('pem-file') - const decPem = pem.decode(Buffer.from(AttestationCertificatePEM)); - return decPem; + return pem.decode(Buffer.from(AttestationCertificatePEM)); } diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 4878189..30216ea 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -18,6 +18,20 @@ export class AttestationObjectWrapper { } } +export class AssertionResponse { + public authenticatorData: Uint8Array + public signature: Uint8Array + public userHandle: Uint8Array + public credentialId: string + + constructor(credId: string, authData: Uint8Array, sign: Uint8Array, userHandle: Uint8Array) { + this.authenticatorData = authData; + this.signature = sign; + this.userHandle = userHandle; + this.credentialId = credId; + } +} + export class Authenticator { private static AAGUID: Uint8Array = new Uint8Array([ 1214244733, 1205845608, 840015201, 3897052717, @@ -30,6 +44,57 @@ export class Authenticator { return 0; } + public static async authenticatorGetAssertion(rpId: string, + hash: Uint8Array, + requireUserPresence: boolean, + requireUserVerification: boolean, + allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[], + extensions?: any + ): Promise { + + log.debug('Called authenticatorGetAssertion'); + + // Step 2-7 + // Note: The authenticator won't let the user select a public key credential source + let credSource: PublicKeyCredentialSource = null; + if (allowCredentialDescriptorList) { + for (let i = 0; i < allowCredentialDescriptorList.length; i++) { + const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; + const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); + credSource = await CredentialsMap.lookup(rpId, credId); + if (credSource != null) { + break; + } + } + } else { + throw new Error(`No allowCredentialDescriptorList provided.`); + } + if (credSource == null) { + throw new Error(`Container does not manage any of the credentials in allowCredentialDescriptorList.`); + } + // ToDo User consent + + // Step 8 + // ToDo Include Extension Processing + const processedExtensions = undefined; + + // Step 9: The current version does not increment counter + + // Step 10 + const authenticatorData = await this.generateAuthenticatorData(rpId, + this.getSignatureCounter(), undefined, processedExtensions); + + // Step 11 + const concatData = new Uint8Array(authenticatorData.length + hash.length); + concatData.set(authenticatorData); + concatData.set(hash, authenticatorData.length); + const prvKey = await ECDSA.fromKey(credSource.privateKey); + const signature = await prvKey.sign(concatData); + + // Step 13 + return new AssertionResponse(credSource.id, authenticatorData, signature, credSource.userHandle); + } + public static async authenticatorMakeCredential(hash: Uint8Array, rpEntity: PublicKeyCredentialRpEntity, userEntity: PublicKeyCredentialUserEntity, @@ -59,25 +124,26 @@ export class Authenticator { for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - if (credMapEntries.findIndex(x => x.id == credId) >= 0) { + if (credMapEntries.findIndex(x => + (x.id == credId) && (x.type === excludeCredentialDescriptorList[i].type)) >= 0) { throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); } } } - // Step 6 - // ToDo User Consent - // Step 5 if (requireUserVerification) { throw new Error(`authenticator does not support user verification`); } + // Step 6 + // ToDo User Consent + // Step 7 - const credentialId = this.createCredentialId() + const credentialId = this.createCredentialId(); const keyPair = await ECDSA.createECDSAKeyPair(); - const credSrc = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id) // No User Handle - await CredentialsMap.put(rpEntity.id, credSrc); + const credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id); // No user Handle + await CredentialsMap.put(rpEntity.id, credentialSource); // Step 9 // ToDo Include Extension Processing diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 4e7a677..be841e2 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -1,35 +1,46 @@ -import {base64ToByteArray, byteArrayToBase64} from "./utils"; +import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from "./utils"; import {Authenticator} from "./webauthn_authenticator"; import {getLogger} from "./logging"; type FunctionType = string; -const Create: FunctionType = "webauthn.create" +const Create: FunctionType = "webauthn.create"; +const Get: FunctionType = "webauthn.get"; const log = getLogger('webauthn_authenticator'); export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean): Promise { log.debug('Called createPublicKeyCredential'); - if (options.publicKey.attestation) { - if (options.publicKey.attestation !== "direct") { - throw new Error(`Only direct attestation supported`); - } + + // Step 2 + if (!sameOriginWithAncestors) { + throw new Error(`sameOriginWithAncestors has to be true`); } + // Step 7 + const rpID = options.publicKey.rp.id || getDomainFromOrigin(origin); + + // Step 11 + // ToDo clientExtensions + authenticatorExtensions + + // Step 13 + 14 const clientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); + // Step 15 const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); const clientDataHash = new Uint8Array(clientDataHashDigest); - const requireUserVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required" + // Step 20: Simplified, just for 1 authenticator + const userVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required"; + const userPresence = !userVerification; const attObjWrapper = await Authenticator.authenticatorMakeCredential(clientDataHash, options.publicKey.rp, options.publicKey.user, options.publicKey.authenticatorSelection.requireResidentKey, - !requireUserVerification, - requireUserVerification, + userPresence, + userVerification, options.publicKey.pubKeyCredParams, - options.publicKey.excludeCredentials) // ToDo Add extensions map + options.publicKey.excludeCredentials); log.debug('Received attestation object'); @@ -45,6 +56,50 @@ export async function createPublicKeyCredential(origin: string, options: Credent } as PublicKeyCredential; } +export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean) { + // Step 2 + if (!sameOriginWithAncestors) { + throw new Error(`sameOriginWithAncestors has to be true`); + } + + // Step 7 + const rpID = options.publicKey.rpId || getDomainFromOrigin(origin); + + // Step 8 + 9 + // ToDo Each authenticator extension is an client extension! + + // Step 10 + 11 + const clientDataJSON = generateClientDataJSON(Get, options.publicKey.challenge as ArrayBuffer, origin); + + // Step 12 + const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); + const clientDataHash = new Uint8Array(clientDataHashDigest); + + // Step 18: Simplified, just for 1 authenticator + const userVerification = options.publicKey.userVerification === "required"; + const userPresence = !userVerification; + const assertionCreationData = await Authenticator.authenticatorGetAssertion(options.publicKey.rpId, + clientDataHash, + userPresence, + userVerification, + options.publicKey.allowCredentials); + + log.debug('Received assertion response'); + + return { + getClientExtensionResults: () => ({}), + id: assertionCreationData.credentialId, + rawId: base64ToByteArray(assertionCreationData.credentialId, true), + response: { + authenticatorData: assertionCreationData.authenticatorData.buffer, + clientDataJSON: base64ToByteArray(window.btoa(JSON.stringify(clientDataJSON))), + signature: assertionCreationData.signature.buffer, + userHandle: assertionCreationData.userHandle, + }, + type: 'public-key', + } as PublicKeyCredential; +} + function generateClientDataJSON(type: FunctionType, challenge: ArrayBuffer, origin: string, tokenBinding?: string): any { return { type: type, From 29ff52d1d6d13cc604fa098f95626d694ec933d0 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 24 Aug 2020 20:20:36 +0200 Subject: [PATCH 43/81] Clean up --- dist/chromium/popup.html | 7 +- src/background.ts | 88 +++----- src/constants.ts | 8 +- src/content_script.ts | 5 +- src/crypto.ts | 258 ---------------------- src/inject_webauthn.ts | 30 ++- src/options.ts | 3 +- src/popup.ts | 72 ++----- src/recovery.ts | 390 ---------------------------------- src/storage.ts | 201 ------------------ src/utils.ts | 2 +- src/webauth_storage.ts | 4 +- src/webauthn.ts | 176 --------------- src/webauthn_authenticator.ts | 16 +- src/webauthn_client.ts | 12 +- 15 files changed, 91 insertions(+), 1181 deletions(-) delete mode 100644 src/crypto.ts delete mode 100644 src/recovery.ts delete mode 100644 src/storage.ts delete mode 100644 src/webauthn.ts diff --git a/dist/chromium/popup.html b/dist/chromium/popup.html index 29e9940..52bf53e 100644 --- a/dist/chromium/popup.html +++ b/dist/chromium/popup.html @@ -9,12 +9,9 @@
- Please create a 4-digit pin for + Consent for
- - - - +
diff --git a/src/background.ts b/src/background.ts index 4248fbd..36d4fbe 100644 --- a/src/background.ts +++ b/src/background.ts @@ -4,9 +4,6 @@ import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; -import {processCredentialCreation, processCredentialRequest} from './webauthn'; - -import {pskRecovery, pskSetup, setBackupDeviceBaseUrl} from './recovery'; import {createPublicKeyCredential, getPublicKeyCredential} from "./webauthn_client"; const log = getLogger('background'); @@ -15,118 +12,97 @@ chrome.runtime.onInstalled.addListener(() => { log.info('Extension installed'); }); -const pinProtectedCallbacks: { [tabId: number]: (pin: number) => void } = {}; +const userConsentCallbacks: { [tabId: number]: (consent: boolean) => void } = {}; -const requestPin = async (tabId: number, origin: string, newPin: boolean = true): Promise => { +const requestUserConsent = async (tabId: number, origin: string): Promise => { const tabKey = `tab-${tabId}`; - chrome.storage.local.set({ [tabKey]: { origin, newPin } }, () => { + chrome.storage.local.set({ [tabKey]: { origin } }, () => { if (chrome.runtime.lastError) { throw new Error(`failed to store value: ${chrome.runtime.lastError}`); } }); log.debug('setting popup for tab', tabId); - const cb: Promise = new Promise((res, _) => { - pinProtectedCallbacks[tabId] = res; + const cb: Promise = new Promise((res, _) => { + userConsentCallbacks[tabId] = res; }); chrome.pageAction.setIcon({ tabId, path: enabledIcons }); chrome.pageAction.setPopup({ tabId, popup: 'popup.html' }); chrome.pageAction.show(tabId); - const pin = await cb; + const userConsent = await cb; chrome.storage.local.remove(tabKey); chrome.pageAction.setPopup({ tabId, popup: '' }); chrome.pageAction.hide(tabId); chrome.pageAction.setIcon({ tabId, path: disabledIcons }); - return pin; -}; - -const setup = async () => { - log.debug('Setup called'); - await pskSetup(); -}; - -const recovery = async () => { - log.debug('Recovery called!'); - await pskRecovery(); + return userConsent; }; -const saveOptions = async (msg) => { - log.debug('Save options called!'); - setBackupDeviceBaseUrl(msg); -}; - -const create = async (msg, sender: chrome.runtime.MessageSender) => { +const createCredential = async (msg, sender: chrome.runtime.MessageSender) => { if (!sender.tab || !sender.tab.id) { - log.debug('received create event without a tab ID'); + log.debug('received createCredential event without a tab ID'); return; } - + const opts = webauthnParse(msg.options); const origin = getOriginFromUrl(sender.url); - const pin = await requestPin(sender.tab.id, origin); + const userConsentCB = requestUserConsent(sender.tab.id, origin); try { - const opts = webauthnParse(msg.options); const credential = await createPublicKeyCredential( origin, opts, true, + userConsentCB ); return { credential: webauthnStringify(credential), requestID: msg.requestID, - type: 'create_response', + type: 'create_credential_response', }; } catch (e) { log.error('failed to register credential', { errorType: `${(typeof e)}` }, e); } }; -const sign = async (msg, sender: chrome.runtime.MessageSender) => { +const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { + if (!sender.tab || !sender.tab.id) { + log.debug('received getCredential event without a tab ID'); + return; + } const opts = webauthnParse(msg.options); const origin = getOriginFromUrl(sender.url); - const pin = await requestPin(sender.tab.id, origin); + const userConsentCB = requestUserConsent(sender.tab.id, origin); try { - const credential = await getPublicKeyCredential(origin, opts, true); + const credential = await getPublicKeyCredential(origin, opts, true, userConsentCB); return { credential: webauthnStringify(credential), requestID: msg.requestID, - type: 'sign_response', + type: 'get_credential_response', }; } catch (e) { - log.error('failed to create assertion', { errorType: `${(typeof e)}` }, e); + log.error('failed to create credential assertion', { errorType: `${(typeof e)}` }, e); } }; chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg.type) { - case 'create': - create(msg, sender).then(sendResponse); + case 'create_credential': + createCredential(msg, sender).then(sendResponse); break; - case 'sign': - sign(msg, sender).then(sendResponse); + case 'get_credential': + getCredential(msg, sender).then(sendResponse); break; - case 'pin': - const cb = pinProtectedCallbacks[msg.tabId]; + case 'user_consent': + const cb = userConsentCallbacks[msg.tabId]; if (!cb) { - log.warn(`Received pin for tab ${msg.tabId} but no callback registered`); + log.warn(`Received user consent for tab ${msg.tabId} but no callback registered`); } else { - cb(msg.pin); - delete (pinProtectedCallbacks[msg.tabId]); + cb(msg.userConsent); + delete (userConsentCallbacks[msg.tabId]); } break; - case 'setup': - setup().then(() => alert('Backup keys synchronized successfully!'), error => log.error('Setup' + - ' failed', error)) - break; - case 'recovery': - recovery().then(() => alert('Recovery finished successfully!')); - break; - case 'saveOptions': - saveOptions(msg.url).then(() => alert('Saving options successfully!')); - break; + default: sendResponse(null); } - return true; }); diff --git a/src/constants.ts b/src/constants.ts index fc7015a..7658225 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,8 @@ export const enabledIcons = { 128: 'images/lock_enabled-128.png', }; -export const ES256_COSE = -7 -export const ES256 = "P-256" -export const SHA256_COSE = 1 +export const ES256_COSE = -7; +export const ES256 = "P-256"; +export const SHA256_COSE = 1; + +export const PIN = "0000"; diff --git a/src/content_script.ts b/src/content_script.ts index d557263..2cd4493 100644 --- a/src/content_script.ts +++ b/src/content_script.ts @@ -5,7 +5,7 @@ webauthnInject.type = 'text/javascript'; webauthnInject.src = 'chrome-extension://' + chrome.runtime.id + '/js/inject_webauthn.js'; document.documentElement.appendChild(webauthnInject); -const relevantEventTypes = ['create', 'sign']; +const relevantEventTypes = ['create_credential', 'get_credential']; window.addEventListener('message', (event) => { // We only accept messages from this window to itself, no iframes allowed. @@ -16,8 +16,7 @@ window.addEventListener('message', (event) => { // Relay relevant messages only. if (event.data.type && relevantEventTypes.indexOf(event.data.type) > -1) { chrome.runtime.sendMessage(event.data, (resp: any) => { - // The callback function will relay the extension response - // to the window object. + // The callback function will relay the extension response to the window object. window.postMessage({ requestID: resp.requestID, resp, diff --git a/src/crypto.ts b/src/crypto.ts deleted file mode 100644 index 6d8fa87..0000000 --- a/src/crypto.ts +++ /dev/null @@ -1,258 +0,0 @@ -import * as asn1 from 'asn1.js'; -import {BN} from 'bn.js'; -import * as CBOR from 'cbor'; -import {base64ToByteArray, byteArrayToBase64} from './utils'; - -const CKEY_ID = new Uint8Array([ - 1214244733, 1205845608, 840015201, 3897052717, - 4072880437, 4027233456, 675224361, 2305433287, - 74291263, 3461796691, 701523034, 3178201666, - 3992003567, 1410532, 4234129691, 1438515639, -]); - -// Copied from krypton -function counterToBytes(c: number): Uint8Array { - const bytes = new Uint8Array(4); - // Sadly, JS TypedArrays are whatever-endian the platform is, - // so Uint32Array is not at all useful here (or anywhere?), - // and we must manually pack the counter (big endian as per spec). - bytes[0] = 0xFF & c >>> 24; - bytes[1] = 0xFF & c >>> 16; - bytes[2] = 0xFF & c >>> 8; - bytes[3] = 0xFF & c; - return bytes; -} - -const coseEllipticCurveNames: { [s: number]: string } = { - 1: 'SHA-256', -}; - -const ellipticNamedCurvesToCOSE: { [s: string]: number } = { - 'P-256': -7, -}; - -export interface ICOSECompatibleKey { - algorithm: number; - privateKey?: CryptoKey; - publicKey?: CryptoKey; - generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise; - generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, - extensionOutput: Uint8Array): Promise; - sign(clientData: Uint8Array): Promise; - toCOSE(key: CryptoKey): Promise>; -} - -class ECDSA implements ICOSECompatibleKey { - - public static async fromKey(key: CryptoKey): Promise { - if (key.type === 'public') { - return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], null, key); - } else { - return new ECDSA(ellipticNamedCurvesToCOSE[(key.algorithm as EcKeyAlgorithm).namedCurve], key); - } - } - - public static async fromCOSEAlgorithm(algorithm: number): Promise { - // Creating the key - let namedCurve: string; - for (const k in ellipticNamedCurvesToCOSE) { - if (ellipticNamedCurvesToCOSE[k] === algorithm) { - namedCurve = k; - break; - } - } - if (!namedCurve) { - throw new Error(`could not find a named curve for algorithm ${algorithm}`); - } - const keyPair = await window.crypto.subtle.generateKey( - { name: 'ECDSA', namedCurve }, - true, - ['sign'], - ); - return new ECDSA(algorithm, keyPair.privateKey, keyPair.publicKey); - } - - /** - * This maps a COSE algorithm ID https://www.iana.org/assignments/cose/cose.xhtml#algorithms - * to its respective COSE curve ID // Based on https://tools.ietf.org/html/rfc8152#section-13.1. - */ - private static ellipticCurveKeys: { [s: number]: number } = { - [-7]: 1, - }; - - constructor( - public algorithm: number, - public privateKey: CryptoKey, - public publicKey?: CryptoKey, - ) { - if (!(algorithm in ECDSA.ellipticCurveKeys)) { - throw new Error(`unknown ECDSA algorithm ${algorithm}`); - } - } - - public async generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise { - return JSON.stringify({ - challenge: byteArrayToBase64(Buffer.from(challenge), true), - hashAlgorithm: coseEllipticCurveNames[ECDSA.ellipticCurveKeys[this.algorithm]], - ...extraOptions, - }); - } - - public async generateAuthenticatorData(rpID: string, counter: number, credentialId: Uint8Array, - extensionOutput: Uint8Array = null): Promise { - const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); - const rpIdHash = new Uint8Array(rpIdDigest); - - // CKEY_ID is a HAD-specific ID - let aaguid: Uint8Array; - let credIdLen: Uint8Array; - let encodedKey: Uint8Array; - - let authenticatorDataLength = rpIdHash.length + 1 + 4; - if (this.publicKey) { - aaguid = CKEY_ID.slice(0, 16); - // 16-bit unsigned big-endian integer. - credIdLen = new Uint8Array(2); - credIdLen[0] = (credentialId.length >> 8) & 0xff; - credIdLen[1] = credentialId.length & 0xff; - const coseKey = await this.toCOSE(this.publicKey); - encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); - authenticatorDataLength += aaguid.length - + credIdLen.length - + credentialId.length - + encodedKey.length; - } - - if (extensionOutput != null) { - authenticatorDataLength += extensionOutput.byteLength; - } - - const authenticatorData = new Uint8Array(authenticatorDataLength); - let offset = 0; - - // 32 bytes for the RP ID hash - authenticatorData.set(rpIdHash, 0); - offset += rpIdHash.length; - - // 1 byte for flags - authenticatorData[rpIdHash.length] = 1; // User presence (Bit 0) - if (this.publicKey) { - // attestation flag goes on the 7th bit (from the right) - authenticatorData[rpIdHash.length] |= (1 << 6); // Attestation present (Bit 6) - } - if (extensionOutput != null) { - authenticatorData[rpIdHash.length] |= (1 << 7); // Extension present (Bit 7) - } - offset++; - - // 4 bytes for the counter. big-endian uint32 - // https://www.w3.org/TR/webauthn/#signature-counter - authenticatorData.set(counterToBytes(counter), offset); - offset += counterToBytes(counter).length; - - if (!this.publicKey) { - if (extensionOutput != null) { // Extension for assertion - authenticatorData.set(extensionOutput, offset); - } - return authenticatorData; - } - - // attestedCredentialData - - // 16 bytes for the Authenticator Attestation GUID - authenticatorData.set(aaguid, offset); - offset += aaguid.length; - - // 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer. - authenticatorData.set(credIdLen, offset); - offset += credIdLen.length; - - // Variable length authenticator key ID - authenticatorData.set(credentialId, offset); - offset += credentialId.length; - - // Variable length public key - authenticatorData.set(encodedKey, offset); - offset += encodedKey.length; - - // Variable length for extension - if (extensionOutput != null) { - authenticatorData.set(extensionOutput, offset); - } - - return authenticatorData; - } - - public async sign(data: Uint8Array): Promise { - if (!this.privateKey) { - throw new Error('no private key available for signing'); - } - const rawSign = await window.crypto.subtle.sign( - this.getKeyParams(), - this.privateKey, - data, - ); - - const rawSignBuf = new Buffer(rawSign); - - // Credit to: https://stackoverflow.com/a/39651457/5333936 - const ecdsaDerSig = asn1.define('ECPrivateKey', function() { - return this.seq().obj( - this.key('r').int(), - this.key('s').int(), - ); - }); - const r = new BN(rawSignBuf.slice(0, 32).toString('hex'), 16, 'be'); - const s = new BN(rawSignBuf.slice(32).toString('hex'), 16, 'be'); - return ecdsaDerSig.encode({r, s}, 'der'); - } - - public async toCOSE(key: CryptoKey): Promise> { - // In JWK the X and Y portions are Base64URL encoded (https://tools.ietf.org/html/rfc7517#section-3), - // which is just the right type for COSE encoding (https://tools.ietf.org/html/rfc8152#section-7), - // we just need to convert it to a byte array. - const exportedKey = await window.crypto.subtle.exportKey('jwk', key); - const attData = new Map(); - attData.set(1, 2); // EC2 key type - attData.set(3, this.algorithm); - attData.set(-1, ECDSA.ellipticCurveKeys[this.algorithm]); - attData.set(-2, base64ToByteArray(exportedKey.x, true)); - attData.set(-3, base64ToByteArray(exportedKey.y, true)); - return attData; - } - - private getKeyParams(): EcdsaParams { - return { name: 'ECDSA', hash: coseEllipticCurveNames[ECDSA.ellipticCurveKeys[this.algorithm]] }; - } -} - -// ECDSA w/ SHA-256 -const defaultPKParams = { alg: -7, type: 'public-key' }; -const coseAlgorithmToKeyName = { - [-7]: 'ECDSA', -}; - -export const getCompatibleKey = (pkParams: PublicKeyCredentialParameters[]): Promise => { - for (const params of (pkParams || [defaultPKParams])) { - const algorithmName = coseAlgorithmToKeyName[params.alg]; - if (!algorithmName) { - continue; - } - switch (algorithmName) { - case 'ECDSA': - return ECDSA.fromCOSEAlgorithm(params.alg); - default: - throw new Error(`unsupported key algorithm ${algorithmName}`); - } - } - throw new Error(`unable to get key`); -}; - -export const getCompatibleKeyFromCryptoKey = (key: CryptoKey): Promise => { - switch (key.algorithm.name) { - case 'ECDSA': - return ECDSA.fromKey(key); - default: - throw new Error(`unsupported key algorithm ${key.algorithm.name}`); - } -}; diff --git a/src/inject_webauthn.ts b/src/inject_webauthn.ts index b35d373..b0c8190 100644 --- a/src/inject_webauthn.ts +++ b/src/inject_webauthn.ts @@ -13,20 +13,19 @@ const log = getLogger('inject_webauthn'); const cKeyCredentials: any = {}; cKeyCredentials.create = async (options: CredentialCreationOptions): Promise => { const requestID = ++webauthnReqCounter; - const registerRequest = { + const createCredentialRequest = { options: webauthnStringify(options), requestID, - type: 'create', + type: 'create_credential', }; const cb: Promise = new Promise((res, _) => { webauthnCallbacks[requestID] = res; }); - window.postMessage(registerRequest, window.location.origin); + window.postMessage(createCredentialRequest, window.location.origin); const webauthnResponse = await cb; - // Because "options" contains functions we must stringify it, otherwise - // object cloning is illegal. + // Because "options" contains functions we must stringify it, otherwise object cloning is illegal. const credential = webauthnParse(webauthnResponse.resp.credential); - credential.getClientExtensionResults = () => ({}); + credential.getClientExtensionResults = () => ({}); // ToDo Return actual client extension result credential.__proto__ = window['PublicKeyCredential'].prototype; return credential; }; @@ -36,20 +35,20 @@ const log = getLogger('inject_webauthn'); webauthnCallbacks[requestID] = res; }); - const signRequest = { + const getCredentialRequest = { options: webauthnStringify(options), requestID, - type: 'sign', + type: 'get_credential', }; - window.postMessage(signRequest, window.location.origin); + window.postMessage(getCredentialRequest, window.location.origin); const webauthnResponse = await cb; const credential = webauthnParse(webauthnResponse.resp.credential); - credential.getClientExtensionResults = () => ({}); + credential.getClientExtensionResults = () => ({}); // ToDo Return actual client extension result credential.__proto__ = window['PublicKeyCredential'].prototype; return credential; }; - const hybridCredentials = { + const hybridCredentials = { // Support native WebAuthn as well as cKey async create(options) { log.debug('created called'); const credentialBackends = [ @@ -58,9 +57,7 @@ const log = getLogger('inject_webauthn'); if (nativeCredentials.create) { credentialBackends.push(nativeCredentials); } - - // We need to bind to the "navigator.credentials" object otherwise - // the browser will be sad. + // Bind to the "navigator.credentials" object return Promise.race(credentialBackends.map((b) => b.create.bind(navigator.credentials)(options))); }, async get(options) { @@ -71,8 +68,7 @@ const log = getLogger('inject_webauthn'); if (nativeCredentials.create) { credentialBackends.push(nativeCredentials); } - // We need to bind to the "navigator.credentials" object otherwise - // the browser will be sad. + // Bind to the "navigator.credentials" object return Promise.race(credentialBackends.map((b) => b.get.bind(navigator.credentials)(options))); }, }; @@ -80,7 +76,7 @@ const log = getLogger('inject_webauthn'); Object.assign(navigator.credentials, hybridCredentials); window.addEventListener('message', (evt) => { const msg = evt.data; - if (['create_response', 'sign_response'].indexOf(msg.type) > -1) { + if (['create_credential_response', 'get_credential_response'].indexOf(msg.type) > -1) { log.debug('relevant message', msg); if (msg.requestID && msg.resp && webauthnCallbacks[msg.requestID]) { webauthnCallbacks[msg.requestID](msg); diff --git a/src/options.ts b/src/options.ts index f53ff05..1e3c595 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,5 +1,4 @@ import $ from 'jquery'; -import {getBackupDeviceBaseUrl} from "./recovery"; $(() => { $('#Setup').on('click', function(evt: Event) { @@ -9,7 +8,7 @@ $(() => { }); }); - $.when(getBackupDeviceBaseUrl()).then((url) => $('#BackupDeviceUrl').val(url)); + //$.when(getBackupDeviceBaseUrl()).then((url) => $('#BackupDeviceUrl').val(url)); $('#Recovery').on('click', function(evt: Event) { evt.preventDefault(); diff --git a/src/popup.ts b/src/popup.ts index e794c04..43055b8 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -10,66 +10,22 @@ $(() => { return; } + $('#userConsent').on('click', function(evt: Event) { + evt.preventDefault(); + evt.stopPropagation(); + + chrome.runtime.sendMessage({ + userConsent: true, + tabId: currentTab.id, + type: 'user_consent', + }); + window.close(); + return false; + }); + const tabKey = `tab-${currentTab.id}`; chrome.storage.local.get([tabKey], (result) => { - log.debug('got storage results', result); - const pinPromise: Promise = new Promise((res, _) => { - $('#domain').text(result[tabKey].origin); - $('input').first().focus(); - prepareInputs(res); - }); - pinPromise.then((pin) => { - log.debug('continue with pin', pin); - chrome.runtime.sendMessage({ - pin, - tabId: currentTab.id, - type: 'pin', - }); - window.close(); - }); + $('#domain').text(result[tabKey].origin); }); }); - - // Inspired by https://codepen.io/nirarazi/pen/ZGovQo - const prepareInputs = (res: (n: number) => void) => { - const body = $('body'); - - const goToNextInput = (e: JQueryEventObject) => { - const key = e.which; - const t = $(e.target); - const sib = t.next('input'); - if (key !== 9 && (key < 48 || key > 57)) { - e.preventDefault(); - return false; - } - if (key === 9) { - return true; - } - if (!sib || !sib.length) { - let pin = 0; - $('input', $('body')).each((n, v) => { - pin = pin * 10; - pin += +$(v).val(); - }); - res(pin); - return false; - } - sib.select().focus(); - }; - - const onKeyDown = (e: JQueryEventObject) => { - const key = e.which; - if (key === 9 || (key >= 48 && key <= 57)) { - return true; - } - e.preventDefault(); - return false; - }; - - const onFocus = (e: JQueryEventObject) => $(e.target).select(); - - body.on('keyup', 'input', goToNextInput); - body.on('keydown', 'input', onKeyDown); - body.on('click', 'input', onFocus); - }; }); diff --git a/src/recovery.ts b/src/recovery.ts deleted file mode 100644 index 38dfd34..0000000 --- a/src/recovery.ts +++ /dev/null @@ -1,390 +0,0 @@ -import * as axios from 'axios'; -import * as CBOR from 'cbor'; - -import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin, padString} from './utils'; - -import {fetchExportContainer, saveExportContainer, PublicKeyCredentialSource} from './storage'; - -import {getCompatibleKeyFromCryptoKey} from './crypto'; - -import {getLogger} from './logging'; - -const log = getLogger('recovery'); - -export const PSK: string = 'psk'; - -const BACKUP_DEVICE_URL = 'bd_url'; - -export async function setBackupDeviceBaseUrl(url: string) { - await saveExportContainer(CONFIG, new Array(new ExportContainer(BACKUP_DEVICE_URL, url))); -} - -export async function getBackupDeviceBaseUrl(): Promise { - const ct = await fetchExportContainer(CONFIG).catch(_ => Array()); - const config = ct.filter((c) => c.id === BACKUP_DEVICE_URL); - return config.length !== 0 ? config[0].payload : 'http://localhost:8005'; -} - -export type ExportContainerType = string; -const BACKUP: ExportContainerType = 'backup'; -const RECOVERY: ExportContainerType = 'recovery'; -const DELEGATION: ExportContainerType = 'delegation'; -const CONFIG: ExportContainerType = 'config'; - -export async function pskSetup(): Promise { - const authAlias = prompt('Please enter a name for your authenticator', 'MyAuth'); - const keyAmount: number = +prompt('How many backup keys should be created?', '5'); - - const url = await getBackupDeviceBaseUrl(); - - return await axios.default.post(url + '/setup', {authAlias, keyAmount}) - .then(async function(response) { - log.debug(response); - const stpRsp = response.data; - let i; - const container = new Array(); - for (i = 0; i < stpRsp.length; ++i) { - const jwk = stpRsp[i].publicBackupKey; - const attObj = stpRsp[i].attObj; - const parsedKey = await parseJWK(jwk, []); - const credId = base64ToByteArray(stpRsp[i].credId, true); - const encId = byteArrayToBase64(credId, true); - const bckpKey = new BackupKey(parsedKey, encId, attObj); - const expBckpKey = await bckpKey.export(); - container.push(expBckpKey); - } - log.debug('Loaded backup keys', container); - - await saveExportContainer(BACKUP, container); - }); -} - -export async function pskRecovery() { - const authId = prompt('Which authenticator you want to replace?', 'MyAuth'); - - const url = await getBackupDeviceBaseUrl(); - - await axios.default.get(url + '/recovery?authId=' + authId) - .then(async function(response1) { - log.debug(response1); - const keyAmount = response1.data.keyAmount; - - const rkData = await RecoveryKey.generate(keyAmount); - - await axios.default.post(url + '/recovery', {rkData, authId}) - .then(async function(response2) { - log.debug(response2); - const rawDelegations = response2.data; - - let i; - const container = new Array(); - for (i = 0; i < rawDelegations.length; ++i) { - const sign = base64ToByteArray(rawDelegations[i].sign, true); - const encSign = byteArrayToBase64(sign, true); - const srcCredId = base64ToByteArray(rawDelegations[i].srcCredId, true); - const encSrcCredId = byteArrayToBase64(srcCredId, true); - const dstCredId = base64ToByteArray(rawDelegations[i].dstCredId, true); - const encDstCredId = byteArrayToBase64(dstCredId, true); - const del = new Delegation(encSign, encSrcCredId, encDstCredId); - container.push(del.export()); - } - log.debug('Loaded delegation', container); - await saveExportContainer(DELEGATION, container); - }) - .catch(function(error) { - log.error(error); - }); - }) - .catch(function(error) { - log.error(error); - }); -} - -export class ExportContainer { - public id: string; - public payload: string; - - constructor(id: string, payload: string) { - this.id = id; - this.payload = payload; - } -} - -export class BackupKey { - public static async import(kx: ExportContainer): Promise { - const json = JSON.parse(kx.payload); - const key = await parseJWK(json.parsedKey, []); - return new BackupKey(key, kx.id, json.attObj); - } - - public static async get(): Promise { - const container = await fetchExportContainer(BACKUP); - if (container.length === 0) { - throw new Error(`No backup key available`); - } - const key = container.pop(); - await saveExportContainer(BACKUP, container); - log.debug(`${container.length} backup keys left`); - - return await BackupKey.import(key); - } - - public publicBackupKey: CryptoKey; - public backupDeviceAttObj: string; - public credentialId: string; - - constructor(key: CryptoKey, id: string, attObj: string) { - this.publicBackupKey = key; - this.credentialId = id; - this.backupDeviceAttObj = attObj; - } - - public async export(): Promise { - const jwk = await window.crypto.subtle.exportKey('jwk', this.publicBackupKey); - const rawJSON = {parsedKey: jwk, attObj: this.backupDeviceAttObj}; - return new ExportContainer(this.credentialId, JSON.stringify(rawJSON)); - } -} - -export class RecoveryKey { - public static async import(kx: ExportContainer): Promise { - const json = JSON.parse(kx.payload); - const key = await parseJWK(json.parsedKey, ['sign']); - const attObj = base64ToByteArray(json.parsedAttObj, true); - - return new RecoveryKey(kx.id, key, attObj); - } - - public static async generate(n: number): Promise { - const delSetup = new Array(); - const container = new Array(); - let i; - for (i = 0; i < n; ++i) { - const keyPair = await window.crypto.subtle.generateKey( - { name: 'ECDSA', namedCurve: 'P-256' }, - true, - ['sign'], - ); - - const bckKey = await BackupKey.get(); - const pubRk = await getCompatibleKeyFromCryptoKey(keyPair.publicKey); - const pskSetup = await createPSKSetupExtensionOutput(bckKey); - const authData = await pubRk.generateAuthenticatorData('', 0, base64ToByteArray(bckKey.credentialId, true), pskSetup); - const attObj = CBOR.encodeCanonical({ - attStmt: new Map(), - authData, - fmt: 'none', - }); - - const exportRk = await (new RecoveryKey(bckKey.credentialId, keyPair.privateKey, attObj)).export(); - container.push(exportRk); - - delSetup.push(new ExportContainer(exportRk.id, padString(byteArrayToBase64(attObj, true)))); - } - - await saveExportContainer(RECOVERY, container); - - return delSetup; - } - - public key: CryptoKey; - public id: string; - public attObj: Uint8Array; - - constructor(id: string, key: CryptoKey, attObj: Uint8Array) { - this.id = id; - this.key = key; - this.attObj = attObj; - } - - public async export(): Promise { - const parsedKey = await window.crypto.subtle.exportKey('jwk', this.key); - const parsedAttObj = byteArrayToBase64(this.attObj, true); - const rawJSON = {parsedKey, parsedAttObj}; - - return new ExportContainer(this.id, JSON.stringify(rawJSON)); - } -} - -class Delegation { - public static import(kx: ExportContainer): Delegation { - return JSON.parse(kx.payload); - } - - public static async getById(srcCredId: string): Promise { - const container = await fetchExportContainer(DELEGATION); - log.debug('Fetched delegations', container); - const del = container.filter((x) => x.id === srcCredId); - return del.length !== 0 ? (Delegation.import(del[0])) : null; - } - - public sign: string; - public srcCredId: string; - public dstCredId: string; - - constructor(sign, srcCredId, dstCredId) { - this.srcCredId = srcCredId; - this.sign = sign; - this.dstCredId = dstCredId; - } - - public export(): ExportContainer { - return new ExportContainer(this.srcCredId, JSON.stringify(this)); - } -} - -class RecoveryMessage { - public del: Delegation; - public rk: RecoveryKey; - - constructor(delegation: Delegation, rk: RecoveryKey) { - this.del = delegation; - this.rk = rk; - } - - public encode(): ArrayBuffer { - return CBOR.encodeCanonical({ - attestationObject: this.rk.attObj, - delSign: this.del.sign, - srcCredId: this.del.srcCredId, - }).buffer; - } -} - -async function parseJWK(jwk, usages): Promise { - return window.crypto.subtle.importKey( - 'jwk', - jwk, - { - name: 'ECDSA', - namedCurve: 'P-256', - }, - true, - usages, - ); -} - -export async function createPSKSetupExtensionOutput(backupKey: BackupKey): Promise { - const stpMsg = CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.backupDeviceAttObj, true)}); - const extOutput = new Map([[PSK, stpMsg]]); - return new Uint8Array(CBOR.encodeCanonical(extOutput)); -} - -async function createPSKRecoveryExtensionOutput(recMsg: RecoveryMessage): Promise { - const extOutput = new Map([[PSK, recMsg.encode()]]); - return new Uint8Array(CBOR.encodeCanonical(extOutput)); -} - -async function getRecoveryKey(id: string): Promise { - const container = await fetchExportContainer(RECOVERY); - log.debug(container); - const rk = container.filter((x) => x.id === id); - return rk.length !== 0 ? (await RecoveryKey.import(rk[0])) : null; -} - -class RecoveryOptions { - public rk: RecoveryKey; - public del: Delegation; - - constructor(rk: RecoveryKey, del: Delegation) { - this.del = del; - this.rk = rk; - } -} - -async function getRecoveryOptions(srcCredId: string): Promise { - const del = await Delegation.getById(srcCredId); - log.debug('Use delegation', del); - if (del === null) { - return null; - } - const rk = await getRecoveryKey(del.dstCredId); - log.debug('Use recovery key', rk); - if (rk === null) { - return null; - } - return new RecoveryOptions(rk, del); -} - -export const recover = async ( - origin: string, - publicKeyRequestOptions: PublicKeyCredentialRequestOptions, - pin: string, -): Promise => { - if (!publicKeyRequestOptions.allowCredentials) { - log.debug('No keys requested'); - return null; - } - - let srcCredId: ArrayBuffer; - let encSrcCredId; - let i; - let recOps; - let requestedCredential; - for (i = 0; i < publicKeyRequestOptions.allowCredentials.length; i++) { - requestedCredential = publicKeyRequestOptions.allowCredentials[i]; - srcCredId = requestedCredential.id as ArrayBuffer; - encSrcCredId = byteArrayToBase64(new Uint8Array(srcCredId), true); - - recOps = await getRecoveryOptions(encSrcCredId); - - if (recOps !== null) { - break; - } - } - if (!recOps) { - throw new Error(`no recovery options available for credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)}`); - } - - log.info('Started recovery for', encSrcCredId); - log.debug('Recovery options', recOps); - - const rkId = base64ToByteArray(recOps.rk.id, true); - const encRkId = byteArrayToBase64(rkId, true); - - const rkPrv = await getCompatibleKeyFromCryptoKey(recOps.rk.key); - - const recMessage = new RecoveryMessage(recOps.del, recOps.rk); - log.debug('Recovery message', recMessage); - const extOutput = await createPSKRecoveryExtensionOutput(recMessage); - - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - - const publicKeyCredentialSource = new PublicKeyCredentialSource(encRkId, rkPrv.privateKey, rpID, null); - await publicKeyCredentialSource.store( pin); - - const clientData = await rkPrv.generateClientData( - publicKeyRequestOptions.challenge as ArrayBuffer, - { - origin, - tokenBinding: { - status: 'not-supported', - }, - type: 'webauthn.get', - }, - ); - const clientDataJSON = base64ToByteArray(window.btoa(clientData)); - const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); - - const authenticatorData = await rkPrv.generateAuthenticatorData(rpID, 0, new Uint8Array(), - new Uint8Array(extOutput)); - - const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); - concatData.set(authenticatorData); - concatData.set(clientDataHash, authenticatorData.length); - - const signature = await rkPrv.sign(concatData); - - return { - getClientExtensionResults: () => ({}), - id: encRkId, - rawId: rkId, - response: { - authenticatorData: authenticatorData.buffer, - clientDataJSON, - signature: (new Uint8Array(signature)).buffer, - userHandle: new ArrayBuffer(0), // This should be nullable - }, - type: 'public-key', - } as PublicKeyCredential; -}; diff --git a/src/storage.ts b/src/storage.ts deleted file mode 100644 index 74c1589..0000000 --- a/src/storage.ts +++ /dev/null @@ -1,201 +0,0 @@ -import {ivLength, keyExportFormat, saltLength} from './constants'; - -import {base64ToByteArray, byteArrayToBase64, concatenate} from './utils'; - -import {getLogger} from './logging'; - -import {ExportContainer, ExportContainerType} from './recovery'; - -const log = getLogger('storage'); - -// https://www.w3.org/TR/webauthn/#public-key-credential-source -export class PublicKeyCredentialSource { - public static async exits (id: string): Promise { - return new Promise(async (res, rej) => { - chrome.storage.sync.get({[id]: null}, async (resp) => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - } else { - res(!(resp[id] == null)); - } - }); - }); - }; - - public static async load(id: string, pin: string): Promise { - log.debug('Loading public key credential source for',id); - return new Promise(async (res, rej) => { - chrome.storage.sync.get({[id]: null}, async (resp) => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - return; - } - if (resp[id] == null) { - return rej('Public key credential source not found'); - } - - const json = JSON.parse(resp[id]); - - const _id = json.id; - const _rpId = json.rpId; - const _userHandle = json.userHandle; - - const keyPayload = base64ToByteArray(json.privateKey); - const saltByteLength = keyPayload[0]; - const ivByteLength = keyPayload[1]; - const keyAlgorithmByteLength = keyPayload[2]; - let offset = 3; - const salt = keyPayload.subarray(offset, offset + saltByteLength); - offset += saltByteLength; - const iv = keyPayload.subarray(offset, offset + ivByteLength); - offset += ivByteLength; - const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength); - offset += keyAlgorithmByteLength; - const keyBytes = keyPayload.subarray(offset); - - const wrappingKey = await getWrappingKey(pin, salt); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); - const _privateKey = await window.crypto.subtle.unwrapKey( - keyExportFormat, - keyBytes, - wrappingKey, - wrapAlgorithm, - unwrappingKeyAlgorithm, - true, - ['sign'], - ); - res(new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle)); - }); - }); - } - - public id: string - public privateKey: CryptoKey - public rpId: string - public userHandle: string - public type: string - - constructor(id: string, privateKey: CryptoKey, rpId: string, userHandle: string) { - this.id = id; - this.privateKey = privateKey; - this.rpId = rpId; - this.userHandle = userHandle; - this.type = "public-key"; - } - - public async store(pin: string): Promise { - return new Promise(async (res, rej) => { - if (!pin) { - rej('no pin provided'); - return; - } - const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(pin, salt); - const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - - const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( - keyExportFormat, - this.privateKey, - wrappingKey, - wrapAlgorithm, - ); - const wrappedKey = new Uint8Array(wrappedKeyBuffer); - const keyAlgorithm = new TextEncoder().encode(JSON.stringify(this.privateKey.algorithm)); - const payload = concatenate( - Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), - salt, - iv, - keyAlgorithm, - wrappedKey); - - const json = { - id: this.id, - privateKey: byteArrayToBase64(payload), - rpId: this.rpId, - userHandle: this.userHandle, - type: this.type - } - - chrome.storage.sync.set({[this.id]: JSON.stringify(json)}, () => { - if (!!chrome.runtime.lastError) { - rej(chrome.runtime.lastError); - } else { - res(); - } - }); - }); - } -} - -const getWrappingKey = async (pin: string, salt: Uint8Array): Promise => { - const enc = new TextEncoder(); - const derivationKey = await window.crypto.subtle.importKey( - 'raw', - enc.encode(pin), - {name: 'PBKDF2', length: 256}, - false, - ['deriveBits', 'deriveKey'], - ); - const pbkdf2Params: Pbkdf2Params = { - hash: 'SHA-256', - iterations: 100000, - name: 'PBKDF2', - salt, - }; - return window.crypto.subtle.deriveKey( - pbkdf2Params, - derivationKey, - {name: 'AES-GCM', length: 256}, - true, - ['wrapKey', 'unwrapKey'], - ); -}; - -export async function saveExportContainer(cType: ExportContainerType, container: ExportContainer[]): Promise { - const exportJSON = JSON.stringify(container); - - log.debug(`Storing ${cType} container`, exportJSON); - - return new Promise(async (res, rej) => { - chrome.storage.local.set({[cType]: exportJSON}, () => { - if (!!chrome.runtime.lastError) { - log.error('Could not store container', chrome.runtime.lastError.message); - rej(chrome.runtime.lastError); - } else { - res(); - } - }); - }); -} - -export async function fetchExportContainer(cType: ExportContainerType): Promise { - return new Promise(async (res, rej) => { - chrome.storage.local.get({[cType]: null}, async (resp) => { - if (!!chrome.runtime.lastError) { - log.warn(`Could not fetch ${cType} container`); - rej(chrome.runtime.lastError); - return; - } - - if (resp[cType] == null) { - return rej(`Container ${cType} not found`); - } - - const exportJSON = await JSON.parse(resp[cType]); - const exportContainer = new Array(); - let i; - for (i = 0; i < exportJSON.length; ++i) { - exportContainer.push(new ExportContainer(exportJSON[i].id, exportJSON[i].payload)); - } - res(exportContainer); - }); - }); -} diff --git a/src/utils.ts b/src/utils.ts index c796ec3..98f6a9c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,6 @@ export function webauthnStringify(o) { return JSON.stringify(o, (k, v) => { if (v) { if (v.constructor.name === 'ArrayBuffer') { - // Because Buffer.from(ArrayBuffer) was not working on firefox v = new Uint8Array(v); } if (v.constructor.name === 'Uint8Array') { @@ -16,6 +15,7 @@ export function webauthnStringify(o) { return v; }); } + export function webauthnParse(j) { return JSON.parse(j, (k, v) => { if (v && v.kr_ser_ty === 'Uint8Array') { diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 7616e6c..a9c6b31 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,9 +1,9 @@ import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; -import {ivLength, keyExportFormat, saltLength} from "./constants"; +import {ivLength, keyExportFormat, PIN, saltLength} from "./constants"; import {getLogger} from "./logging"; const log = getLogger('auth_storage'); -const PIN = "0000"; + export class CredentialsMap { public static async put(rpId: string, credSrc: PublicKeyCredentialSource): Promise { diff --git a/src/webauthn.ts b/src/webauthn.ts deleted file mode 100644 index 346ce00..0000000 --- a/src/webauthn.ts +++ /dev/null @@ -1,176 +0,0 @@ -import * as CBOR from 'cbor'; - -import {getCompatibleKey, getCompatibleKeyFromCryptoKey} from './crypto'; - -import {getLogger} from './logging'; - -import {PublicKeyCredentialSource} from './storage'; - -import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from './utils'; - -import {BackupKey, createPSKSetupExtensionOutput, PSK, recover} from './recovery'; - -const log = getLogger('webauthn'); - -// Attestation -export const processCredentialCreation = async ( - origin: string, - publicKeyCreationOptions: PublicKeyCredentialCreationOptions, - pin: string, -): Promise => { - if (publicKeyCreationOptions.attestation !== 'none') { - log.warn('We can perform only none attestation'); - return null; - } - - let i; - if (publicKeyCreationOptions.excludeCredentials) { - for (i = 0; i < publicKeyCreationOptions.excludeCredentials.length; i++) { - const requestedCredential = publicKeyCreationOptions.excludeCredentials[i]; - const credId = requestedCredential.id as ArrayBuffer; - const encCredId = byteArrayToBase64(new Uint8Array(credId), true); - - const publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); - - if (publicKeyCredentialSource) { - throw new Error(`authenticator manages credential contained in excludeCredentials option.`); - } - } - } - - let supportRecovery = false; - const reqExt: any = publicKeyCreationOptions.extensions; - if (reqExt !== undefined) { - if (reqExt.hasOwnProperty(PSK)) { - supportRecovery = true; - log.info('RP supports PSK'); - } - } - - const rp = publicKeyCreationOptions.rp; - const rpID = rp.id || getDomainFromOrigin(origin); - - const bckpKey = await BackupKey.get(); - log.info('Use backup key', bckpKey); - - const credId = base64ToByteArray(bckpKey.credentialId, true); - const encCredId = byteArrayToBase64(credId, true); - - if (await PublicKeyCredentialSource.exits(encCredId)) { - throw new Error(`credential with id ${encCredId} already exists`); - } - - const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams); - - let extOutput = null; - if (supportRecovery) { - extOutput = await createPSKSetupExtensionOutput(bckpKey); - } - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId, extOutput); - - const clientData = await compatibleKey.generateClientData( - publicKeyCreationOptions.challenge as ArrayBuffer, - { origin, type: 'webauthn.create' }, - ); - - const attestationObject = CBOR.encodeCanonical({ - attStmt: new Map(), - authData: authenticatorData, - fmt: 'none', - }).buffer; - - const publicKeyCredentialSource = new PublicKeyCredentialSource(encCredId, compatibleKey.privateKey, rpID, null) - await publicKeyCredentialSource.store(pin); - - log.debug('Attestation created'); - - return { - getClientExtensionResults: () => ({}), - id: encCredId, - rawId: credId, - response: { - attestationObject, - clientDataJSON: base64ToByteArray(window.btoa(clientData)), - }, - type: 'public-key', - } as PublicKeyCredential; -}; - -// Assertion -export const processCredentialRequest = async ( - origin: string, - publicKeyRequestOptions: PublicKeyCredentialRequestOptions, - pin: string, -): Promise => { - if (!publicKeyRequestOptions.allowCredentials) { - log.debug('No credentials requested'); - return null; - } - - const reqExt: any = publicKeyRequestOptions.extensions; - if (reqExt !== undefined) { - if (reqExt.hasOwnProperty(PSK)) { - log.debug('Recovery requested'); - return await recover(origin, publicKeyRequestOptions, pin); - } - } - - let i; - let publicKeyCredentialSource: PublicKeyCredentialSource; - let credId: ArrayBuffer; - let encCredId; - for (i = 0; i < publicKeyRequestOptions.allowCredentials.length; i++) { - const requestedCredential = publicKeyRequestOptions.allowCredentials[i]; - credId = requestedCredential.id as ArrayBuffer; - encCredId = byteArrayToBase64(new Uint8Array(credId), true); - - publicKeyCredentialSource = await PublicKeyCredentialSource.load(encCredId, pin).catch((_) => null); - - if (publicKeyCredentialSource) { - break; - } - } - if (!publicKeyCredentialSource) { - throw new Error(`credential with id ${JSON.stringify(publicKeyRequestOptions.allowCredentials)} not found`); - } - - const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin); - - const compatibleKey = await getCompatibleKeyFromCryptoKey(publicKeyCredentialSource.privateKey); - const clientData = await compatibleKey.generateClientData( - publicKeyRequestOptions.challenge as ArrayBuffer, - { - origin, - tokenBinding: { - status: 'not-supported', - }, - type: 'webauthn.get', - }, - ); - const clientDataJSON = base64ToByteArray(window.btoa(clientData)); - const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON)); - - const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array(), null); - - // Prepare input for signature - const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length); - concatData.set(authenticatorData); - concatData.set(clientDataHash, authenticatorData.length); - - const signature = await compatibleKey.sign(concatData); - log.debug('signature', signature); - log.debug('clientData', clientData); - - return { - getClientExtensionResults: () => ({}), - id: encCredId, - rawId: credId, - response: { - authenticatorData: authenticatorData.buffer, - clientDataJSON, - signature: (new Uint8Array(signature)).buffer, - userHandle: new ArrayBuffer(0), - }, - type: 'public-key', - } as PublicKeyCredential; -}; diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 30216ea..3f951a1 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -44,7 +44,8 @@ export class Authenticator { return 0; } - public static async authenticatorGetAssertion(rpId: string, + public static async authenticatorGetAssertion(userConsentCallback: Promise, + rpId: string, hash: Uint8Array, requireUserPresence: boolean, requireUserVerification: boolean, @@ -72,7 +73,10 @@ export class Authenticator { if (credSource == null) { throw new Error(`Container does not manage any of the credentials in allowCredentialDescriptorList.`); } - // ToDo User consent + const userConsent = await userConsentCallback; + if (!userConsent) { + throw new Error(`no user consent`); + } // Step 8 // ToDo Include Extension Processing @@ -95,7 +99,8 @@ export class Authenticator { return new AssertionResponse(credSource.id, authenticatorData, signature, credSource.userHandle); } - public static async authenticatorMakeCredential(hash: Uint8Array, + public static async authenticatorMakeCredential(userConsentCallback: Promise, + hash: Uint8Array, rpEntity: PublicKeyCredentialRpEntity, userEntity: PublicKeyCredentialUserEntity, requireResidentKey: boolean, @@ -137,7 +142,10 @@ export class Authenticator { } // Step 6 - // ToDo User Consent + const userConsent = await userConsentCallback; + if (!userConsent) { + throw new Error(`no user consent`); + } // Step 7 const credentialId = this.createCredentialId(); diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index be841e2..1517d8c 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -8,7 +8,7 @@ const Get: FunctionType = "webauthn.get"; const log = getLogger('webauthn_authenticator'); -export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean): Promise { +export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise): Promise { log.debug('Called createPublicKeyCredential'); // Step 2 @@ -17,7 +17,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent } // Step 7 - const rpID = options.publicKey.rp.id || getDomainFromOrigin(origin); + options.publicKey.rp.id = options.publicKey.rp.id || getDomainFromOrigin(origin); // Step 11 // ToDo clientExtensions + authenticatorExtensions @@ -33,7 +33,8 @@ export async function createPublicKeyCredential(origin: string, options: Credent const userVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required"; const userPresence = !userVerification; - const attObjWrapper = await Authenticator.authenticatorMakeCredential(clientDataHash, + const attObjWrapper = await Authenticator.authenticatorMakeCredential(userConsentCallback, + clientDataHash, options.publicKey.rp, options.publicKey.user, options.publicKey.authenticatorSelection.requireResidentKey, @@ -56,7 +57,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent } as PublicKeyCredential; } -export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean) { +export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise) { // Step 2 if (!sameOriginWithAncestors) { throw new Error(`sameOriginWithAncestors has to be true`); @@ -78,7 +79,8 @@ export async function getPublicKeyCredential(origin: string, options: Credential // Step 18: Simplified, just for 1 authenticator const userVerification = options.publicKey.userVerification === "required"; const userPresence = !userVerification; - const assertionCreationData = await Authenticator.authenticatorGetAssertion(options.publicKey.rpId, + const assertionCreationData = await Authenticator.authenticatorGetAssertion(userConsentCallback, + rpID, clientDataHash, userPresence, userVerification, From 9523e7880984776c23deeeaf7f940f07419a20bd Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 24 Aug 2020 20:45:32 +0200 Subject: [PATCH 44/81] Make credential source lookup during assertion more web authn conform --- src/webauthn_authenticator.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 3f951a1..c5d0faa 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -56,23 +56,26 @@ export class Authenticator { log.debug('Called authenticatorGetAssertion'); // Step 2-7 - // Note: The authenticator won't let the user select a public key credential source - let credSource: PublicKeyCredentialSource = null; + let credentialOptions: PublicKeyCredentialSource[] = []; if (allowCredentialDescriptorList) { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - credSource = await CredentialsMap.lookup(rpId, credId); - if (credSource != null) { - break; + const cred = await CredentialsMap.lookup(rpId, credId); + if (cred != null) { + credentialOptions.push(cred); } } } else { - throw new Error(`No allowCredentialDescriptorList provided.`); + credentialOptions = credentialOptions.concat(await CredentialsMap.load(rpId)); } - if (credSource == null) { - throw new Error(`Container does not manage any of the credentials in allowCredentialDescriptorList.`); + if (credentialOptions.length == 0) { + throw new Error(`Container does not manage any related credentials`); } + // Note: The authenticator won't let the user select a public key credential source + const credSource = credentialOptions[0]; + + const userConsent = await userConsentCallback; if (!userConsent) { throw new Error(`no user consent`); From 419a2fbf1ced061acef2ceb1124afb1816d6448d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Tue, 25 Aug 2020 20:26:19 +0200 Subject: [PATCH 45/81] Added basic structure for PSK registration --- src/background.ts | 23 ++++++++ src/constants.ts | 9 ++- src/options.ts | 9 +-- src/webauth_storage.ts | 104 +++++++++++++++++++++++++++++++--- src/webauthn_authenticator.ts | 25 ++++++-- src/webauthn_client.ts | 30 ++++++++-- src/webauthn_psk.ts | 70 +++++++++++++++++++++++ 7 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 src/webauthn_psk.ts diff --git a/src/background.ts b/src/background.ts index 36d4fbe..7dd4355 100644 --- a/src/background.ts +++ b/src/background.ts @@ -5,6 +5,7 @@ import {getLogger} from './logging'; import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {createPublicKeyCredential, getPublicKeyCredential} from "./webauthn_client"; +import {PSK} from "./webauthn_psk"; const log = getLogger('background'); @@ -83,6 +84,22 @@ const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { } }; +const pskSetup = async () => { + try { + await PSK.setup(); + } catch (e) { + log.error('failed to setup psk', { errorType: `${(typeof e)}` }, e); + } +}; + +const pskOptions = async (url) => { + try { + await PSK.setOptions(url); + } catch (e) { + log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e); + } +}; + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg.type) { case 'create_credential': @@ -91,6 +108,12 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'get_credential': getCredential(msg, sender).then(sendResponse); break; + case 'psk_setup': + pskSetup().then(() => alert('PSK setup was successfully!'), null); + break; + case 'psk_options': + pskOptions(msg.url).then(() => alert('PSK options was successfully!'), null); + break; case 'user_consent': const cb = userConsentCallbacks[msg.tabId]; if (!cb) { diff --git a/src/constants.ts b/src/constants.ts index 7658225..d76a3c4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,7 +17,12 @@ export const enabledIcons = { }; export const ES256_COSE = -7; -export const ES256 = "P-256"; +export const ES256 = 'P-256'; export const SHA256_COSE = 1; -export const PIN = "0000"; +export const PIN = '0000'; + +export const PSK_EXTENSION_IDENTIFIER = 'psk'; +export const BACKUP_KEY = 'backup_key'; +export const BD_ENDPOINT = 'bd_endpoint'; +export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005'; diff --git a/src/options.ts b/src/options.ts index 1e3c595..d7b2f1f 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,26 +1,27 @@ import $ from 'jquery'; +import {PSK} from "./webauthn_psk"; $(() => { $('#Setup').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'setup', + type: 'psk_setup', }); }); - //$.when(getBackupDeviceBaseUrl()).then((url) => $('#BackupDeviceUrl').val(url)); + $.when(PSK.bdDeviceUrl()).then((url) => $('#BackupDeviceUrl').val(url)); $('#Recovery').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'recovery', + type: 'psk_recovery', }); }); $('#SaveBackupDeviceUrl').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'saveOptions', + type: 'psk_options', url: $('#BackupDeviceUrl').val(), }); }); diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index a9c6b31..a7f416f 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,14 +1,106 @@ import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; -import {ivLength, keyExportFormat, PIN, saltLength} from "./constants"; +import {BACKUP_KEY, BD_ENDPOINT, DEFAULT_BD_ENDPOINT, ivLength, keyExportFormat, PIN, saltLength} from "./constants"; import {getLogger} from "./logging"; +import {BackupKey} from "./webauthn_psk"; const log = getLogger('auth_storage'); +export class PSKStorage { + public static async getBDEndpoint(): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.get({[BD_ENDPOINT]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.getBDEndpoint', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[BACKUP_KEY] == null) { + log.warn(`No endpoint found, use default endpoint`); + res(DEFAULT_BD_ENDPOINT); + } + log.debug('Loaded BD endpoint successfully'); + res(resp[BACKUP_KEY]); + }); + }); + } + + public static async setBDEndpoint(endpoint: string): Promise { + log.debug('Set BD endpoint to', endpoint); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[BD_ENDPOINT]: endpoint}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.setBDEndpoint', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); + } + + public static async storeBackupKeys(backupKeys: BackupKey[], override: boolean = false): Promise { + log.debug(`Storing backup keys`); + const backupKeysExists = await this.existBackupKeys(); + if (backupKeysExists && !override) { + log.debug('Backup keys already exist. Update entry.'); + const entries = await this.loadBackupKeys(); + backupKeys = entries.concat(backupKeys); + } + + let exportJSON = JSON.stringify(backupKeys); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[BACKUP_KEY]: exportJSON}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.storeBackupKeys', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + } else { + res(); + } + }); + }); + }; + + public static async loadBackupKeys(): Promise { + log.debug(`Loading backup keys`); + return new Promise(async (res, rej) => { + chrome.storage.local.get({[BACKUP_KEY]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.loadBackupKeys', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[BACKUP_KEY] == null) { + log.warn(`No backup keys found`); + res([]); + } + + const backupKeys = await JSON.parse(resp[BACKUP_KEY]); + log.debug('Loaded backup keys successfully'); + res(backupKeys); + }); + }); + } + + private static async existBackupKeys(): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.get({[BACKUP_KEY]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.existBackupKeys', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + } else { + res(!(resp[BACKUP_KEY] == null)); + } + }); + }); + }; +} export class CredentialsMap { public static async put(rpId: string, credSrc: PublicKeyCredentialSource): Promise { log.debug(`Storing credential map entry for ${rpId}`); - const mapEntryExists = await this.exits(rpId); + const mapEntryExists = await this.exists(rpId); let credSrcs: PublicKeyCredentialSource[]; if (mapEntryExists) { log.debug('Credential map entry does already exist. Update entry.'); @@ -75,11 +167,11 @@ export class CredentialsMap { } } - public static async exits(rpId: string): Promise { + public static async exists(rpId: string): Promise { return new Promise(async (res, rej) => { chrome.storage.local.get({[rpId]: null}, async (resp) => { if (!!chrome.runtime.lastError) { - log.error('Could not perform CredentialsMap.exits', chrome.runtime.lastError.message); + log.error('Could not perform CredentialsMap.exists', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); } else { res(!(resp[rpId] == null)); @@ -91,7 +183,6 @@ export class CredentialsMap { export class PublicKeyCredentialSource { public static async import(json: any): Promise { - log.debug('Import PublicKeyCredentialSource', json); const _id = json.id; const _rpId = json.rpId; const _userHandle = json.userHandle; @@ -124,7 +215,7 @@ export class PublicKeyCredentialSource { true, ['sign'], ); - log.debug('Imported PublicKeyCredentialSource with id', _id) + return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle); } @@ -178,7 +269,6 @@ export class PublicKeyCredentialSource { type: this.type } - log.debug('Exported PublicKeyCredentialSource with id', this.id) return json; } } diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index c5d0faa..7bb5cec 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -4,7 +4,8 @@ import {base64ToByteArray, byteArrayToBase64, counterToBytes} from "./utils"; import * as CBOR from 'cbor'; import {createAttestationSignature, getAttestationCertificate} from "./webauthn_attestation"; import {getLogger} from "./logging"; -import {ES256_COSE} from "./constants"; +import {ES256_COSE, PSK_EXTENSION_IDENTIFIER} from "./constants"; +import {PSK} from "./webauthn_psk"; const log = getLogger('webauthn_authenticator'); @@ -111,7 +112,7 @@ export class Authenticator { requireUserVerification: boolean, credTypesAndPubKeyAlgs: PublicKeyCredentialParameters[], excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[], - extensions?: any): Promise { + extensions?: Map): Promise { log.debug('Called authenticatorMakeCredential'); // Step 2 @@ -157,8 +158,22 @@ export class Authenticator { await CredentialsMap.put(rpEntity.id, credentialSource); // Step 9 - // ToDo Include Extension Processing - const extensionData = undefined; + let processedExtensions = undefined; + if (extensions) { + log.debug(extensions); + if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { + log.debug('PSK requested'); + const pskOutPut = await PSK.authenticatorGetCredentialExtensionOutput(); + log.debug('PSK extension output created'); + // ToDo Check if input is cbor encoded null + processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); + // ToDo Return credId from PSK to replace created credential id + } + } + if (processedExtensions) { + processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); + } + // Step 10 const sigCnt = this.getSignatureCounter(); @@ -168,7 +183,7 @@ export class Authenticator { const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); // Step 12 - const authenticatorData = await this.generateAuthenticatorData(rpEntity.id, sigCnt, attestedCredentialData, extensionData); + const authenticatorData = await this.generateAuthenticatorData(rpEntity.id, sigCnt, attestedCredentialData, processedExtensions); // Step 13 const attObj = await this.generateAttestationObject(hash, authenticatorData); diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 1517d8c..c780794 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -1,12 +1,14 @@ +import * as CBOR from 'cbor'; import {base64ToByteArray, byteArrayToBase64, getDomainFromOrigin} from "./utils"; import {Authenticator} from "./webauthn_authenticator"; import {getLogger} from "./logging"; +import {PSK_EXTENSION_IDENTIFIER} from "./constants"; type FunctionType = string; const Create: FunctionType = "webauthn.create"; const Get: FunctionType = "webauthn.get"; -const log = getLogger('webauthn_authenticator'); +const log = getLogger('webauthn_client'); export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise): Promise { log.debug('Called createPublicKeyCredential'); @@ -20,7 +22,19 @@ export async function createPublicKeyCredential(origin: string, options: Credent options.publicKey.rp.id = options.publicKey.rp.id || getDomainFromOrigin(origin); // Step 11 - // ToDo clientExtensions + authenticatorExtensions + const clientExtensions = undefined; // ToDo clientExtensions + let authenticatorExtensions = undefined; + if (options.publicKey.extensions) { + const reqExt: any = options.publicKey.extensions; + if (reqExt.hasOwnProperty(PSK_EXTENSION_IDENTIFIER)) { + log.debug('PSK extension requested'); + if (reqExt[PSK_EXTENSION_IDENTIFIER] == true) { + log.debug('PSK extension has valid client input'); + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); + authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + } + } + } // Step 13 + 14 const clientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); @@ -30,18 +44,24 @@ export async function createPublicKeyCredential(origin: string, options: Credent const clientDataHash = new Uint8Array(clientDataHashDigest); // Step 20: Simplified, just for 1 authenticator - const userVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required"; + let userVerification = false; + let residentKey = false; + if (options.publicKey.authenticatorSelection) { + userVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required"; + residentKey = options.publicKey.authenticatorSelection.requireResidentKey; + } const userPresence = !userVerification; const attObjWrapper = await Authenticator.authenticatorMakeCredential(userConsentCallback, clientDataHash, options.publicKey.rp, options.publicKey.user, - options.publicKey.authenticatorSelection.requireResidentKey, + residentKey, userPresence, userVerification, options.publicKey.pubKeyCredParams, - options.publicKey.excludeCredentials); + options.publicKey.excludeCredentials, + authenticatorExtensions); log.debug('Received attestation object'); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts new file mode 100644 index 0000000..147db74 --- /dev/null +++ b/src/webauthn_psk.ts @@ -0,0 +1,70 @@ +import * as axios from 'axios'; +import * as CBOR from 'cbor'; + +import {PSKStorage} from "./webauth_storage"; +import {getLogger} from "./logging"; +import {base64ToByteArray} from "./utils"; + +const log = getLogger('webauthn_psk'); + +export class BackupKey { + public credentialId: string; + public bdAttObj: string; // base64 URL + + constructor(credId: string, attObj: string) { + this.credentialId = credId; + this.bdAttObj = attObj; + } +} + +export class PSK { + public static async bdDeviceUrl(): Promise { + return await PSKStorage.getBDEndpoint(); + } + + public static async setOptions(url: string): Promise { + return await PSKStorage.setBDEndpoint(url); + } + + public static async setup(): Promise { + const bdEndpoint = await PSKStorage.getBDEndpoint(); + const authAlias = prompt('Please enter an alias name for your authenticator', 'MyAuth'); + const keyAmount: number = +prompt('How many backup keys should be created?', '5'); + + return await axios.default.post(bdEndpoint + '/setup', {authAlias, keyAmount}) + .then(async function(response) { + log.debug(response); + const setupResponse = response.data; + let i; + const backupKeys = new Array(); + for (i = 0; i < setupResponse.length; ++i) { + const backupKey = new BackupKey(setupResponse[i].attObj, setupResponse[i].credId); + backupKeys.push(backupKey); + } + log.debug('Loaded backup keys', backupKeys); + + await PSKStorage.storeBackupKeys(backupKeys); + }); + } + + public static async authenticatorGetCredentialExtensionOutput(): Promise { + const backupKey = await this.popBackupKey(); + return CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)}); + } + + private static async popBackupKey(): Promise { + const backupKeys = await PSKStorage.loadBackupKeys(); + if (backupKeys.length == 0) { + throw new Error('No backup keys available'); + } + const backupKey = backupKeys.pop(); + log.debug('Pop backup key', backupKey); + await PSKStorage.storeBackupKeys(backupKeys); + + return backupKey; + } + + public static async authenticatorMakeCredentialExtensionOutput(): Promise { + return Promise.resolve(undefined); // ToDo Implement + } +} \ No newline at end of file From 6aa4a47b35b75a85e61ab735396f49d68af8b588 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 26 Aug 2020 18:00:35 +0200 Subject: [PATCH 46/81] Fix PSK encoding --- src/webauth_storage.ts | 8 ++++++++ src/webauthn_authenticator.ts | 20 +++++++++++++------- src/webauthn_psk.ts | 12 ++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index a7f416f..c8d5ade 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -18,6 +18,7 @@ export class PSKStorage { if (resp[BACKUP_KEY] == null) { log.warn(`No endpoint found, use default endpoint`); res(DEFAULT_BD_ENDPOINT); + return; } log.debug('Loaded BD endpoint successfully'); res(resp[BACKUP_KEY]); @@ -32,6 +33,7 @@ export class PSKStorage { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.setBDEndpoint', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); + return; } else { res(); } @@ -54,6 +56,7 @@ export class PSKStorage { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.storeBackupKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); + return; } else { res(); } @@ -74,6 +77,7 @@ export class PSKStorage { if (resp[BACKUP_KEY] == null) { log.warn(`No backup keys found`); res([]); + return; } const backupKeys = await JSON.parse(resp[BACKUP_KEY]); @@ -89,6 +93,7 @@ export class PSKStorage { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.existBackupKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); + return; } else { res(!(resp[BACKUP_KEY] == null)); } @@ -124,6 +129,7 @@ export class CredentialsMap { if (!!chrome.runtime.lastError) { log.error('Could not perform CredentialsMap.put', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); + return; } else { res(); } @@ -143,6 +149,7 @@ export class CredentialsMap { if (resp[rpId] == null) { log.warn(`CredentialsMap entry ${rpId} not found`); res([]); + return; } const exportJSON = await JSON.parse(resp[rpId]); @@ -173,6 +180,7 @@ export class CredentialsMap { if (!!chrome.runtime.lastError) { log.error('Could not perform CredentialsMap.exists', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); + return; } else { res(!(resp[rpId] == null)); } diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 7bb5cec..50b8bf8 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -152,9 +152,9 @@ export class Authenticator { } // Step 7 - const credentialId = this.createCredentialId(); + let credentialId = this.createCredentialId(); const keyPair = await ECDSA.createECDSAKeyPair(); - const credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id); // No user Handle + let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id); // No user Handle await CredentialsMap.put(rpEntity.id, credentialSource); // Step 9 @@ -163,11 +163,17 @@ export class Authenticator { log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('PSK requested'); - const pskOutPut = await PSK.authenticatorGetCredentialExtensionOutput(); - log.debug('PSK extension output created'); - // ToDo Check if input is cbor encoded null - processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); - // ToDo Return credId from PSK to replace created credential id + if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null + log.warn('PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); + } else { + const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); + processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); + credentialId = backupKeyCredentialId; + credentialSource.id = credentialId; + await CredentialsMap.put(rpEntity.id, credentialSource); + log.debug('Processed PSK'); + } + } } if (processedExtensions) { diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 147db74..8919c2a 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -9,7 +9,7 @@ const log = getLogger('webauthn_psk'); export class BackupKey { public credentialId: string; - public bdAttObj: string; // base64 URL + public bdAttObj: string; // base64 URL with padding constructor(credId: string, attObj: string) { this.credentialId = credId; @@ -38,7 +38,7 @@ export class PSK { let i; const backupKeys = new Array(); for (i = 0; i < setupResponse.length; ++i) { - const backupKey = new BackupKey(setupResponse[i].attObj, setupResponse[i].credId); + const backupKey = new BackupKey(setupResponse[i].credId, setupResponse[i].attObj); backupKeys.push(backupKey); } log.debug('Loaded backup keys', backupKeys); @@ -47,9 +47,9 @@ export class PSK { }); } - public static async authenticatorGetCredentialExtensionOutput(): Promise { + public static async authenticatorMakeCredentialExtensionOutput(): Promise<[string, Uint8Array]> { const backupKey = await this.popBackupKey(); - return CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)}); + return [backupKey.credentialId, CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)})]; } private static async popBackupKey(): Promise { @@ -59,12 +59,12 @@ export class PSK { } const backupKey = backupKeys.pop(); log.debug('Pop backup key', backupKey); - await PSKStorage.storeBackupKeys(backupKeys); + await PSKStorage.storeBackupKeys(backupKeys, true); return backupKey; } - public static async authenticatorMakeCredentialExtensionOutput(): Promise { + public static async authenticatorGetCredentialExtensionOutput(): Promise { return Promise.resolve(undefined); // ToDo Implement } } \ No newline at end of file From 35d0d7dd55dc757995af72eda8ac0fd622415e7f Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 26 Aug 2020 20:01:44 +0200 Subject: [PATCH 47/81] Create client extension output during credential creation --- src/background.ts | 1 + src/inject_webauthn.ts | 3 ++- src/webauthn_client.ts | 7 ++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/background.ts b/src/background.ts index 7dd4355..6adb988 100644 --- a/src/background.ts +++ b/src/background.ts @@ -55,6 +55,7 @@ const createCredential = async (msg, sender: chrome.runtime.MessageSender) => { ); return { credential: webauthnStringify(credential), + clientExtensionResults: credential.getClientExtensionResults(), requestID: msg.requestID, type: 'create_credential_response', }; diff --git a/src/inject_webauthn.ts b/src/inject_webauthn.ts index b0c8190..357081c 100644 --- a/src/inject_webauthn.ts +++ b/src/inject_webauthn.ts @@ -25,7 +25,8 @@ const log = getLogger('inject_webauthn'); const webauthnResponse = await cb; // Because "options" contains functions we must stringify it, otherwise object cloning is illegal. const credential = webauthnParse(webauthnResponse.resp.credential); - credential.getClientExtensionResults = () => ({}); // ToDo Return actual client extension result + credential.getClientExtensionResults = () => (webauthnResponse.resp.clientExtensionResults); + // extension result credential.__proto__ = window['PublicKeyCredential'].prototype; return credential; }; diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index c780794..6cf29a2 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -22,7 +22,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent options.publicKey.rp.id = options.publicKey.rp.id || getDomainFromOrigin(origin); // Step 11 - const clientExtensions = undefined; // ToDo clientExtensions + let clientExtensions = undefined; let authenticatorExtensions = undefined; if (options.publicKey.extensions) { const reqExt: any = options.publicKey.extensions; @@ -32,6 +32,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent log.debug('PSK extension has valid client input'); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; } } } @@ -66,7 +67,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent log.debug('Received attestation object'); return { - getClientExtensionResults: () => ({}), + getClientExtensionResults: () => (clientExtensions), id: attObjWrapper.credentialId, rawId: base64ToByteArray(attObjWrapper.credentialId, true), response: { @@ -87,7 +88,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential const rpID = options.publicKey.rpId || getDomainFromOrigin(origin); // Step 8 + 9 - // ToDo Each authenticator extension is an client extension! + // ToDo Authenticator Extension // Step 10 + 11 const clientDataJSON = generateClientDataJSON(Get, options.publicKey.challenge as ArrayBuffer, origin); From e2021fc79bdff7f320f7b41a06fc004f6bbec486 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 27 Aug 2020 12:05:03 +0200 Subject: [PATCH 48/81] Add recovery setup --- src/background.ts | 11 ++ src/constants.ts | 1 + src/webauth_storage.ts | 207 ++++++++++++++++++++++++---------- src/webauthn_authenticator.ts | 4 +- src/webauthn_crypto.ts | 2 +- src/webauthn_psk.ts | 112 +++++++++++++++--- 6 files changed, 259 insertions(+), 78 deletions(-) diff --git a/src/background.ts b/src/background.ts index 6adb988..af76b32 100644 --- a/src/background.ts +++ b/src/background.ts @@ -93,6 +93,14 @@ const pskSetup = async () => { } }; +const pskRecovery = async () => { + try { + await PSK.recoverySetup(); + } catch (e) { + log.error('failed to setup psk recovery', { errorType: `${(typeof e)}` }, e); + } +} + const pskOptions = async (url) => { try { await PSK.setOptions(url); @@ -112,6 +120,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'psk_setup': pskSetup().then(() => alert('PSK setup was successfully!'), null); break; + case 'psk_recovery': + pskRecovery().then(() => alert('PSK recovery setup was successfully!'), null); + break; case 'psk_options': pskOptions(msg.url).then(() => alert('PSK options was successfully!'), null); break; diff --git a/src/constants.ts b/src/constants.ts index d76a3c4..fe2b32c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,3 +26,4 @@ export const PSK_EXTENSION_IDENTIFIER = 'psk'; export const BACKUP_KEY = 'backup_key'; export const BD_ENDPOINT = 'bd_endpoint'; export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005'; +export const RECOVERY_KEY = 'recovery_key'; diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index c8d5ade..ed53b77 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,7 +1,16 @@ import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; -import {BACKUP_KEY, BD_ENDPOINT, DEFAULT_BD_ENDPOINT, ivLength, keyExportFormat, PIN, saltLength} from "./constants"; +import { + BACKUP_KEY, + BD_ENDPOINT, + DEFAULT_BD_ENDPOINT, ES256, + ivLength, + keyExportFormat, + PIN, + RECOVERY_KEY, + saltLength +} from "./constants"; import {getLogger} from "./logging"; -import {BackupKey} from "./webauthn_psk"; +import {BackupKey, RecoveryKey} from "./webauthn_psk"; const log = getLogger('auth_storage'); @@ -100,6 +109,81 @@ export class PSKStorage { }); }); }; + + public static async storeRecoveryKeys(recoveryKeys: RecoveryKey[]): Promise { + log.debug('Storing recovery keys'); + + // Export recoveryKeys + const exportKeys = [] + for (let i = 0; i < recoveryKeys.length; i++) { + const recKey = recoveryKeys[i]; + const expPrvKey = await exportKey(recKey.privKey); + const expPubKey = await window.crypto.subtle.exportKey('jwk', recKey.pubKey); + + const json = { + credentialId: recKey.credentialId, + pubKey: expPubKey, + privKey: expPrvKey, + delegationSignature: recKey.delegationSignature, + } + + exportKeys.push(json) + } + + let exportJSON = JSON.stringify(exportKeys); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[RECOVERY_KEY]: exportJSON}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.storeRecoveryKeys', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } else { + res(); + } + }); + }); + } + + public static async loadRecoveryKeys(rpId): Promise { + log.debug(`Loading recovery keys`); + return new Promise(async (res, rej) => { + chrome.storage.local.get({[RECOVERY_KEY]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.loadRecoveryKeys', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[RECOVERY_KEY] == null) { + log.warn(`No recovery keys found`); + res([]); + return; + } + + const exportJSON = await JSON.parse(resp[RECOVERY_KEY]); + const recKeys = new Array(); + for (let i = 0; i < exportJSON.length; ++i) { + const json = exportJSON[i]; + const prvKey = await importKey(json.privKey); + const pubKey = await window.crypto.subtle.importKey( + 'jwk', + json.pubKey, + { + name: 'ECDSA', + namedCurve: ES256, + }, + true, + ['sign'], + ); + + const recKey = new RecoveryKey(json.credId, pubKey, prvKey, json.sign); + recKeys.push(recKey); + } + log.debug('Loaded recovery keys successfully'); + res(recKeys); + }); + }); + } } export class CredentialsMap { @@ -194,35 +278,7 @@ export class PublicKeyCredentialSource { const _id = json.id; const _rpId = json.rpId; const _userHandle = json.userHandle; - - const keyPayload = base64ToByteArray(json.privateKey); - const saltByteLength = keyPayload[0]; - const ivByteLength = keyPayload[1]; - const keyAlgorithmByteLength = keyPayload[2]; - let offset = 3; - const salt = keyPayload.subarray(offset, offset + saltByteLength); - offset += saltByteLength; - const iv = keyPayload.subarray(offset, offset + ivByteLength); - offset += ivByteLength; - const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength); - offset += keyAlgorithmByteLength; - const keyBytes = keyPayload.subarray(offset); - - const wrappingKey = await getWrappingKey(PIN, salt); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); - const _privateKey = await window.crypto.subtle.unwrapKey( - keyExportFormat, - keyBytes, - wrappingKey, - wrapAlgorithm, - unwrappingKeyAlgorithm, - true, - ['sign'], - ); + const _privateKey = await importKey(json.privateKey); return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle); } @@ -246,41 +302,74 @@ export class PublicKeyCredentialSource { } public async export(): Promise { - const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(PIN, salt); - const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); - const wrapAlgorithm: AesGcmParams = { - iv, - name: 'AES-GCM', - }; - - const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( - keyExportFormat, - this.privateKey, - wrappingKey, - wrapAlgorithm, - ); - const wrappedKey = new Uint8Array(wrappedKeyBuffer); - const keyAlgorithm = new TextEncoder().encode(JSON.stringify(this.privateKey.algorithm)); - const payload = concatenate( - Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), - salt, - iv, - keyAlgorithm, - wrappedKey); - - const json = { + return { id: this.id, - privateKey: byteArrayToBase64(payload), + privateKey: await exportKey(this.privateKey), rpId: this.rpId, userHandle: this.userHandle, type: this.type - } - - return json; + }; } } +async function exportKey(key: CryptoKey): Promise { + const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); + const wrappingKey = await getWrappingKey(PIN, salt); + const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + + const wrappedKeyBuffer = await window.crypto.subtle.wrapKey( + keyExportFormat, + key, + wrappingKey, + wrapAlgorithm, + ); + const wrappedKey = new Uint8Array(wrappedKeyBuffer); + const keyAlgorithm = new TextEncoder().encode(JSON.stringify(key.algorithm)); + const payload = concatenate( + Uint8Array.of(saltLength, ivLength, keyAlgorithm.length), + salt, + iv, + keyAlgorithm, + wrappedKey); + + return byteArrayToBase64(payload) +} + +async function importKey(rawKey: string): Promise { + const keyPayload = base64ToByteArray(rawKey); + const saltByteLength = keyPayload[0]; + const ivByteLength = keyPayload[1]; + const keyAlgorithmByteLength = keyPayload[2]; + let offset = 3; + const salt = keyPayload.subarray(offset, offset + saltByteLength); + offset += saltByteLength; + const iv = keyPayload.subarray(offset, offset + ivByteLength); + offset += ivByteLength; + const keyAlgorithmBytes = keyPayload.subarray(offset, offset + keyAlgorithmByteLength); + offset += keyAlgorithmByteLength; + const keyBytes = keyPayload.subarray(offset); + + const wrappingKey = await getWrappingKey(PIN, salt); + const wrapAlgorithm: AesGcmParams = { + iv, + name: 'AES-GCM', + }; + const unwrappingKeyAlgorithm = JSON.parse(new TextDecoder().decode(keyAlgorithmBytes)); + return await window.crypto.subtle.unwrapKey( + keyExportFormat, + keyBytes, + wrappingKey, + wrapAlgorithm, + unwrappingKeyAlgorithm, + true, + ['sign'], + ); +} + const getWrappingKey = async (pin: string, salt: Uint8Array): Promise => { const enc = new TextEncoder(); const derivationKey = await window.crypto.subtle.importKey( diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 50b8bf8..23cf840 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -200,12 +200,12 @@ export class Authenticator { } - private static async generateAttestedCredentialData(credentialId: Uint8Array, keyPair: ICOSECompatibleKey): Promise { + private static async generateAttestedCredentialData(credentialId: Uint8Array, publicKey: ICOSECompatibleKey): Promise { const aaguid = this.AAGUID.slice(0, 16); const credIdLen = new Uint8Array(2); credIdLen[0] = (credentialId.length >> 8) & 0xff; credIdLen[1] = credentialId.length & 0xff; - const coseKey = await keyPair.toCOSE(keyPair.publicKey); + const coseKey = await publicKey.toCOSE(publicKey.publicKey); const encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); const attestedCredentialDataLength = aaguid.length + credIdLen.length + credentialId.length + encodedKey.length; diff --git a/src/webauthn_crypto.ts b/src/webauthn_crypto.ts index 992996b..29203c9 100644 --- a/src/webauthn_crypto.ts +++ b/src/webauthn_crypto.ts @@ -13,7 +13,7 @@ export interface ICOSECompatibleKey { export class ECDSA implements ICOSECompatibleKey { public algorithm: number - public privateKey: CryptoKey + public privateKey?: CryptoKey public publicKey?: CryptoKey public static async fromKey(key: CryptoKey): Promise { diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 8919c2a..c6da8b9 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -3,7 +3,9 @@ import * as CBOR from 'cbor'; import {PSKStorage} from "./webauth_storage"; import {getLogger} from "./logging"; -import {base64ToByteArray} from "./utils"; +import {base64ToByteArray, byteArrayToBase64} from "./utils"; +import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; +import {getAttestationCertificate} from "./webauthn_attestation"; const log = getLogger('webauthn_psk'); @@ -15,6 +17,32 @@ export class BackupKey { this.credentialId = credId; this.bdAttObj = attObj; } + + static async popBackupKey(): Promise { + const backupKeys = await PSKStorage.loadBackupKeys(); + if (backupKeys.length == 0) { + throw new Error('No backup keys available'); + } + const backupKey = backupKeys.pop(); + log.debug('Pop backup key', backupKey); + await PSKStorage.storeBackupKeys(backupKeys, true); + + return backupKey; + } +} + +export class RecoveryKey { + public credentialId: string + public pubKey: CryptoKey + public privKey: CryptoKey + public delegationSignature: string + + constructor(credId: string, pubKey: CryptoKey, privKey: CryptoKey, sign: string) { + this.credentialId = credId; + this.pubKey = pubKey; + this.privKey = privKey; + this.delegationSignature = sign; + } } export class PSK { @@ -35,9 +63,8 @@ export class PSK { .then(async function(response) { log.debug(response); const setupResponse = response.data; - let i; const backupKeys = new Array(); - for (i = 0; i < setupResponse.length; ++i) { + for (let i = 0; i < setupResponse.length; ++i) { const backupKey = new BackupKey(setupResponse[i].credId, setupResponse[i].attObj); backupKeys.push(backupKey); } @@ -47,21 +74,74 @@ export class PSK { }); } - public static async authenticatorMakeCredentialExtensionOutput(): Promise<[string, Uint8Array]> { - const backupKey = await this.popBackupKey(); - return [backupKey.credentialId, CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)})]; - } + public static async recoverySetup(): Promise { - private static async popBackupKey(): Promise { - const backupKeys = await PSKStorage.loadBackupKeys(); - if (backupKeys.length == 0) { - throw new Error('No backup keys available'); - } - const backupKey = backupKeys.pop(); - log.debug('Pop backup key', backupKey); - await PSKStorage.storeBackupKeys(backupKeys, true); + const authAlias = prompt('Which authenticator you want to recover?', 'MyAuth'); + const bdEndpoint = await PSKStorage.getBDEndpoint(); - return backupKey; + return await axios.default.get(bdEndpoint + '/recovery?authAlias=' + authAlias) + .then(async function(initResponse) { + log.debug(initResponse); + const keyAmount = initResponse.data.keyAmount; + + let rawRecKeys = new Array<[string, CryptoKeyPair]>() + let replacementKeys = [] + for (let i = 0; i < keyAmount; i++) { + const keyPair = await window.crypto.subtle.generateKey( + {name: 'ECDSA', namedCurve: 'P-256'}, + true, + ['sign'], + ); + rawRecKeys.push([i.toString(), keyPair]); + + // Prepare delegation request + const pubKey = await ECDSA.fromKey(keyPair.publicKey); + const cosePubKey = await pubKey.toCOSE(pubKey.publicKey); + const encodedPubKey = new Uint8Array(CBOR.encodeCanonical(cosePubKey)); + replacementKeys.push({keyId: i.toString(), replacementPubKey: byteArrayToBase64(encodedPubKey, true)}); + } + + let attCert = byteArrayToBase64(getAttestationCertificate(), true); + + await axios.default.post(bdEndpoint + '/recovery?authAlias=' + authAlias, { + repKeys: replacementKeys, + attCert + }) + .then(async function (delResponse) { + const rawDelegations = delResponse.data; + + let recoveryKeys = new Array() + + for (let i = 0; i < rawDelegations.length; ++i) { + const sign = rawDelegations[i].sign; + const credId = rawDelegations[i].credId; + const keyId = rawDelegations[i].keyId; + + log.debug(rawDelegations[i]); + + const keyPair = rawRecKeys.filter((x, _) => x[0] == keyId); + if (keyPair.length !== 1) { + log.warn('BD response does not contain delegation for key pair', keyId); + continue; + } + + const pubKey = keyPair[0][1].publicKey; + const privKey = keyPair[0][1].privateKey; + + const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign) + + recoveryKeys.push(recoveryKey); + } + + log.debug('Received recovery keys', recoveryKeys); + await PSKStorage.storeRecoveryKeys(recoveryKeys); + }); + }); + } + + public static async authenticatorMakeCredentialExtensionOutput(): Promise<[string, Uint8Array]> { + const backupKey = await BackupKey.popBackupKey(); + return [backupKey.credentialId, CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)})]; } public static async authenticatorGetCredentialExtensionOutput(): Promise { From 27d8008de2d5ca8ef4728df25083dc549635a5a2 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 27 Aug 2020 15:43:53 +0200 Subject: [PATCH 49/81] Add recovery flow on authenticator --- src/webauth_storage.ts | 7 ++- src/webauthn_authenticator.ts | 112 ++++++++++++++++++++++++++++++---- src/webauthn_client.ts | 2 +- src/webauthn_psk.ts | 37 ++++++++++- 4 files changed, 142 insertions(+), 16 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index ed53b77..3dc733c 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -144,7 +144,12 @@ export class PSKStorage { }); } - public static async loadRecoveryKeys(rpId): Promise { + public static async recoveryKeyExists(credId: string): Promise { + const backupKeys = await PSKStorage.loadBackupKeys(); + return backupKeys.filter(x => x.credentialId === credId).length > 0 + } + + public static async loadRecoveryKeys(): Promise { log.debug(`Loading recovery keys`); return new Promise(async (res, rej) => { chrome.storage.local.get({[RECOVERY_KEY]: null}, async (resp) => { diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 23cf840..947e9c6 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -1,5 +1,5 @@ import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; -import {CredentialsMap, PublicKeyCredentialSource} from "./webauth_storage"; +import {CredentialsMap, PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; import {base64ToByteArray, byteArrayToBase64, counterToBytes} from "./utils"; import * as CBOR from 'cbor'; import {createAttestationSignature, getAttestationCertificate} from "./webauthn_attestation"; @@ -51,13 +51,14 @@ export class Authenticator { requireUserPresence: boolean, requireUserVerification: boolean, allowCredentialDescriptorList?: PublicKeyCredentialDescriptor[], - extensions?: any + extensions?: Map ): Promise { log.debug('Called authenticatorGetAssertion'); // Step 2-7 let credentialOptions: PublicKeyCredentialSource[] = []; + let isRecovery: [boolean, string] = [false, ""]; if (allowCredentialDescriptorList) { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; @@ -71,10 +72,26 @@ export class Authenticator { credentialOptions = credentialOptions.concat(await CredentialsMap.load(rpId)); } if (credentialOptions.length == 0) { - throw new Error(`Container does not manage any related credentials`); + // Check if there is any recovery key that matches the provided credential descriptors + for (let i = 0; i < allowCredentialDescriptorList.length; i++) { + const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; + const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); + const recExists = await PSKStorage.recoveryKeyExists(credId); + if (recExists) { + log.info('Recovery detected for', credId); + isRecovery = [true, credId]; + break; + } + } + if (!isRecovery) { + throw new Error(`Container does not manage any related credentials`); + } } // Note: The authenticator won't let the user select a public key credential source - const credSource = credentialOptions[0]; + let credSource; + if (!isRecovery[0]) { + credSource = credentialOptions[0]; + } const userConsent = await userConsentCallback; @@ -83,10 +100,29 @@ export class Authenticator { } // Step 8 - // ToDo Include Extension Processing - const processedExtensions = undefined; + let processedExtensions = undefined; + if (extensions) { + log.debug(extensions); + if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { + log.debug('PSK requested'); + const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); + const pskInput = await CBOR.decode(new Buffer(rawPskInput)); + log.debug('PSK input', pskInput); + const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(null, pskInput.hash, rpId); + processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); + credSource = await CredentialsMap.lookup(rpId, newCredId); + if (credSource == null) { + throw new Error('New credential source missing'); + } + log.debug('Processed PSK'); + + } + } + if (processedExtensions) { + processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); + } - // Step 9: The current version does not increment counter + // Step 9: The current version does not increment the counter // Step 10 const authenticatorData = await this.generateAuthenticatorData(rpId, @@ -151,9 +187,10 @@ export class Authenticator { throw new Error(`no user consent`); } - // Step 7 - let credentialId = this.createCredentialId(); - const keyPair = await ECDSA.createECDSAKeyPair(); + return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, undefined, extensions); + + /*let credentialId = this.createCredentialId(); + let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id); // No user Handle await CredentialsMap.put(rpEntity.id, credentialSource); @@ -196,8 +233,61 @@ export class Authenticator { // Return value is not 1:1 WebAuthn conform log.debug('Created credential', credentialId) - return (new AttestationObjectWrapper(credentialId, attObj)); + return (new AttestationObjectWrapper(credentialId, attObj));*/ + + } + + public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, keyPair?: ICOSECompatibleKey, extensions?: Map): Promise { + // Step 7 + if (!(keyPair)) { + log.debug('No key pair provided, create new one.'); + keyPair = await ECDSA.createECDSAKeyPair(); + } + let credentialId = this.createCredentialId(); + let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpId); // No + // user Handle + await CredentialsMap.put(rpId, credentialSource); + + // Step 9 + let processedExtensions = undefined; + if (extensions) { + log.debug(extensions); + if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { + log.debug('PSK requested'); + if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null + log.warn('PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); + } else { + const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); + processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); + credentialId = backupKeyCredentialId; + credentialSource.id = credentialId; + await CredentialsMap.put(rpId, credentialSource); + log.debug('Processed PSK'); + } + + } + } + if (processedExtensions) { + processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); + } + + + // Step 10 + const sigCnt = this.getSignatureCounter(); + + // Step 11 + const rawCredentialId = base64ToByteArray(credentialId, true); + const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); + // Step 12 + const authenticatorData = await this.generateAuthenticatorData(rpId, sigCnt, attestedCredentialData, processedExtensions); + + // Step 13 + const attObj = await this.generateAttestationObject(hash, authenticatorData); + + // Return value is not 1:1 WebAuthn conform + log.debug('Created credential', credentialId) + return (new AttestationObjectWrapper(credentialId, attObj)); } private static async generateAttestedCredentialData(credentialId: Uint8Array, publicKey: ICOSECompatibleKey): Promise { diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 6cf29a2..790fa82 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -88,7 +88,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential const rpID = options.publicKey.rpId || getDomainFromOrigin(origin); // Step 8 + 9 - // ToDo Authenticator Extension + // ToDo Authenticator Extension, Create custom HASH for psk // Step 10 + 11 const clientDataJSON = generateClientDataJSON(Get, options.publicKey.challenge as ArrayBuffer, origin); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index c6da8b9..cfa3f67 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -1,11 +1,13 @@ import * as axios from 'axios'; import * as CBOR from 'cbor'; -import {PSKStorage} from "./webauth_storage"; +import {PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; import {getLogger} from "./logging"; import {base64ToByteArray, byteArrayToBase64} from "./utils"; import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; import {getAttestationCertificate} from "./webauthn_attestation"; +import {Authenticator} from "./webauthn_authenticator"; +import {PSK_EXTENSION_IDENTIFIER} from "./constants"; const log = getLogger('webauthn_psk'); @@ -43,6 +45,18 @@ export class RecoveryKey { this.privKey = privKey; this.delegationSignature = sign; } + + static async popRecoveryKey(): Promise { + const recoveryKeys = await PSKStorage.loadRecoveryKeys(); + if (recoveryKeys.length == 0) { + throw new Error('No recovery keys available'); + } + const recoveryKey = recoveryKeys.pop(); + log.debug('Pop recovery key', recoveryKey); + await PSKStorage.storeRecoveryKeys(recoveryKeys); + + return recoveryKey; + } } export class PSK { @@ -144,7 +158,24 @@ export class PSK { return [backupKey.credentialId, CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)})]; } - public static async authenticatorGetCredentialExtensionOutput(): Promise { - return Promise.resolve(undefined); // ToDo Implement + public static async authenticatorGetCredentialExtensionOutput(oldCredentialId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { + // Find recovery key for given credential id + const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId === oldCredentialId); + if (recoveryKeys.length !== 1) { + throw new Error(`Expected 1 matching recovery key, but got ${recoveryKeys.length}`); + } + const recKey = recoveryKeys[0]; + + // Create attestation object using the key pair of the recovery key + request PSK extension + const keyPair = await ECDSA.fromKey(recKey.privKey); + keyPair.publicKey = recKey.pubKey; + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); + const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + const attObjWrapper = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); + //const encAttObj = byteArrayToBase64(attObjWrapper.rawAttObj, true); + + const recoveryMessage = {attestationObject: attObjWrapper.rawAttObj, oldCredentialId, delegationSignature: recKey.delegationSignature} + const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); + return [attObjWrapper.credentialId, cborRecMsg] } } \ No newline at end of file From c15b1e32aff1a4fac3e8774875111394ad820bbf Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 28 Aug 2020 12:48:33 +0200 Subject: [PATCH 50/81] Adapt recovery flow --- src/webauth_storage.ts | 8 ++++---- src/webauthn_authenticator.ts | 35 +++++++++++++++++++++++------------ src/webauthn_client.ts | 21 +++++++++++++++++++-- src/webauthn_psk.ts | 33 ++++++++++++++++++++------------- 4 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 3dc733c..590f656 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -145,7 +145,7 @@ export class PSKStorage { } public static async recoveryKeyExists(credId: string): Promise { - const backupKeys = await PSKStorage.loadBackupKeys(); + const backupKeys = await PSKStorage.loadRecoveryKeys(); return backupKeys.filter(x => x.credentialId === credId).length > 0 } @@ -178,13 +178,13 @@ export class PSKStorage { namedCurve: ES256, }, true, - ['sign'], + [], ); - const recKey = new RecoveryKey(json.credId, pubKey, prvKey, json.sign); + const recKey = new RecoveryKey(json.credentialId, pubKey, prvKey, json.delegationSignature); recKeys.push(recKey); } - log.debug('Loaded recovery keys successfully'); + log.debug('Loaded recovery keys successfully', recKeys); res(recKeys); }); }); diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 947e9c6..295fee1 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -56,9 +56,9 @@ export class Authenticator { log.debug('Called authenticatorGetAssertion'); - // Step 2-7 - let credentialOptions: PublicKeyCredentialSource[] = []; + // Step 2-7 + recovery lookup let isRecovery: [boolean, string] = [false, ""]; + let credentialOptions: PublicKeyCredentialSource[] = []; if (allowCredentialDescriptorList) { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; @@ -73,6 +73,7 @@ export class Authenticator { } if (credentialOptions.length == 0) { // Check if there is any recovery key that matches the provided credential descriptors + log.debug('No directly managed credentials found'); for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); @@ -83,13 +84,13 @@ export class Authenticator { break; } } - if (!isRecovery) { + if (!isRecovery[0]) { throw new Error(`Container does not manage any related credentials`); } } // Note: The authenticator won't let the user select a public key credential source let credSource; - if (!isRecovery[0]) { + if (!isRecovery[0]) { // No recovery credSource = credentialOptions[0]; } @@ -104,20 +105,30 @@ export class Authenticator { if (extensions) { log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { - log.debug('PSK requested'); + log.debug('Get: PSK requested'); + if (!isRecovery[0]) { + throw new Error('PSK extension requested, but no matching recovery key available'); + } const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); const pskInput = await CBOR.decode(new Buffer(rawPskInput)); - log.debug('PSK input', pskInput); - const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(null, pskInput.hash, rpId); + log.debug('Get: PSK input', pskInput); + const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery[1], pskInput.hash, rpId); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); credSource = await CredentialsMap.lookup(rpId, newCredId); if (credSource == null) { - throw new Error('New credential source missing'); + // This should never happen + throw new Error('Get: New credential source missing'); } - log.debug('Processed PSK'); + log.debug('Get: Processed PSK'); + } else { } } + + if (!extensions.has(PSK_EXTENSION_IDENTIFIER) && isRecovery[0]) { + throw new Error('Recovery detected, but no PSK requested.') + } + if (processedExtensions) { processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); } @@ -253,16 +264,16 @@ export class Authenticator { if (extensions) { log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { - log.debug('PSK requested'); + log.debug('Make: PSK requested'); if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null - log.warn('PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); + log.warn('Make: PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); } else { const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); credentialId = backupKeyCredentialId; credentialSource.id = credentialId; await CredentialsMap.put(rpId, credentialSource); - log.debug('Processed PSK'); + log.debug('Make: Processed PSK'); } } diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 790fa82..61a3ce2 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -88,7 +88,23 @@ export async function getPublicKeyCredential(origin: string, options: Credential const rpID = options.publicKey.rpId || getDomainFromOrigin(origin); // Step 8 + 9 - // ToDo Authenticator Extension, Create custom HASH for psk + let clientExtensions = undefined; + let authenticatorExtensions = undefined; + if (options.publicKey.extensions) { + const reqExt: any = options.publicKey.extensions; + if (reqExt.hasOwnProperty(PSK_EXTENSION_IDENTIFIER)) { + log.debug('PSK extension requested'); + if (reqExt[PSK_EXTENSION_IDENTIFIER] == true) { + log.debug('PSK extension has valid client input'); + const customClientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); + const customClientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(customClientDataJSON))); + const customClientDataHash = new Uint8Array(customClientDataHashDigest); + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({hash: customClientDataHash})); + authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; // ToDo Add to response + } + } + } // Step 10 + 11 const clientDataJSON = generateClientDataJSON(Get, options.publicKey.challenge as ArrayBuffer, origin); @@ -105,7 +121,8 @@ export async function getPublicKeyCredential(origin: string, options: Credential clientDataHash, userPresence, userVerification, - options.publicKey.allowCredentials); + options.publicKey.allowCredentials, + authenticatorExtensions); log.debug('Received assertion response'); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index cfa3f67..245ee48 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -46,16 +46,18 @@ export class RecoveryKey { this.delegationSignature = sign; } - static async popRecoveryKey(): Promise { - const recoveryKeys = await PSKStorage.loadRecoveryKeys(); + static async findRecoveryKey(credId: string): Promise { + const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId === credId); if (recoveryKeys.length == 0) { - throw new Error('No recovery keys available'); + return null } - const recoveryKey = recoveryKeys.pop(); - log.debug('Pop recovery key', recoveryKey); - await PSKStorage.storeRecoveryKeys(recoveryKeys); - return recoveryKey; + return recoveryKeys[0]; + } + + static async removeRecoveryKey(credId: string): Promise { + const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId !== credId); + return await PSKStorage.storeRecoveryKeys(recoveryKeys); } } @@ -90,7 +92,8 @@ export class PSK { public static async recoverySetup(): Promise { - const authAlias = prompt('Which authenticator you want to recover?', 'MyAuth'); + const authAlias = prompt('Which authenticator you want to recover?', 'OldAuth'); + const newAuthAlias = prompt('What is alias of your current authenticator?', 'MyAuth'); const bdEndpoint = await PSKStorage.getBDEndpoint(); return await axios.default.get(bdEndpoint + '/recovery?authAlias=' + authAlias) @@ -119,7 +122,8 @@ export class PSK { await axios.default.post(bdEndpoint + '/recovery?authAlias=' + authAlias, { repKeys: replacementKeys, - attCert + attCert, + newAuthAlias }) .then(async function (delResponse) { const rawDelegations = delResponse.data; @@ -159,12 +163,12 @@ export class PSK { } public static async authenticatorGetCredentialExtensionOutput(oldCredentialId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { + log.debug('authenticatorGetCredentialExtensionOutput called'); // Find recovery key for given credential id - const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId === oldCredentialId); - if (recoveryKeys.length !== 1) { - throw new Error(`Expected 1 matching recovery key, but got ${recoveryKeys.length}`); + const recKey = await RecoveryKey.findRecoveryKey(oldCredentialId); + if (recKey == null) { + throw new Error("No recovery key found, but recovery were detected"); } - const recKey = recoveryKeys[0]; // Create attestation object using the key pair of the recovery key + request PSK extension const keyPair = await ECDSA.fromKey(recKey.privKey); @@ -174,6 +178,9 @@ export class PSK { const attObjWrapper = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); //const encAttObj = byteArrayToBase64(attObjWrapper.rawAttObj, true); + // Finally remove recovery key since PSK output was generated successfully + await RecoveryKey.removeRecoveryKey(oldCredentialId); + const recoveryMessage = {attestationObject: attObjWrapper.rawAttObj, oldCredentialId, delegationSignature: recKey.delegationSignature} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); return [attObjWrapper.credentialId, cborRecMsg] From 57f176042d6e68e0e2d1eba610a2cea1dbaeabb7 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 28 Aug 2020 12:52:21 +0200 Subject: [PATCH 51/81] Fix recovery input validation --- src/webauthn_authenticator.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 295fee1..2b1fa79 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -120,12 +120,10 @@ export class Authenticator { throw new Error('Get: New credential source missing'); } log.debug('Get: Processed PSK'); - } else { - + } else if (isRecovery[0]) { + throw new Error('Recovery detected, but no PSK requested.') } - } - - if (!extensions.has(PSK_EXTENSION_IDENTIFIER) && isRecovery[0]) { + } else if (isRecovery[0]) { throw new Error('Recovery detected, but no PSK requested.') } From 5383e348600cfd5a22d39ae282bf7d27b126f02d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 30 Aug 2020 13:47:39 +0200 Subject: [PATCH 52/81] Clean up --- src/webauthn_authenticator.ts | 47 ----------------------------------- src/webauthn_client.ts | 2 +- src/webauthn_psk.ts | 5 ++-- 3 files changed, 3 insertions(+), 51 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 2b1fa79..6e1c9e4 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -197,53 +197,6 @@ export class Authenticator { } return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, undefined, extensions); - - /*let credentialId = this.createCredentialId(); - - let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpEntity.id); // No user Handle - await CredentialsMap.put(rpEntity.id, credentialSource); - - // Step 9 - let processedExtensions = undefined; - if (extensions) { - log.debug(extensions); - if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { - log.debug('PSK requested'); - if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null - log.warn('PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); - } else { - const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); - processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); - credentialId = backupKeyCredentialId; - credentialSource.id = credentialId; - await CredentialsMap.put(rpEntity.id, credentialSource); - log.debug('Processed PSK'); - } - - } - } - if (processedExtensions) { - processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); - } - - - // Step 10 - const sigCnt = this.getSignatureCounter(); - - // Step 11 - const rawCredentialId = base64ToByteArray(credentialId, true); - const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); - - // Step 12 - const authenticatorData = await this.generateAuthenticatorData(rpEntity.id, sigCnt, attestedCredentialData, processedExtensions); - - // Step 13 - const attObj = await this.generateAttestationObject(hash, authenticatorData); - - // Return value is not 1:1 WebAuthn conform - log.debug('Created credential', credentialId) - return (new AttestationObjectWrapper(credentialId, attObj));*/ - } public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, keyPair?: ICOSECompatibleKey, extensions?: Map): Promise { diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 61a3ce2..67d0329 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -101,7 +101,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential const customClientDataHash = new Uint8Array(customClientDataHashDigest); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({hash: customClientDataHash})); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; // ToDo Add to response + clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: {clientDataJSON: customClientDataJSON}}; // ToDo Add to response } } } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 245ee48..2ae9418 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -167,7 +167,7 @@ export class PSK { // Find recovery key for given credential id const recKey = await RecoveryKey.findRecoveryKey(oldCredentialId); if (recKey == null) { - throw new Error("No recovery key found, but recovery were detected"); + throw new Error("No recovery key found, but recovery was detected"); } // Create attestation object using the key pair of the recovery key + request PSK extension @@ -176,12 +176,11 @@ export class PSK { const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); const attObjWrapper = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); - //const encAttObj = byteArrayToBase64(attObjWrapper.rawAttObj, true); // Finally remove recovery key since PSK output was generated successfully await RecoveryKey.removeRecoveryKey(oldCredentialId); - const recoveryMessage = {attestationObject: attObjWrapper.rawAttObj, oldCredentialId, delegationSignature: recKey.delegationSignature} + const recoveryMessage = {attestationObject: attObjWrapper.rawAttObj, oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); return [attObjWrapper.credentialId, cborRecMsg] } From 236867b2fccbdc9e70de29effa7dd2f6b5d470f0 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 31 Aug 2020 20:39:36 +0200 Subject: [PATCH 53/81] Fix BD endpoint URL option persistence --- src/webauth_storage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 590f656..3a9a715 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -24,13 +24,13 @@ export class PSKStorage { return; } - if (resp[BACKUP_KEY] == null) { + if (resp[BD_ENDPOINT] == null) { log.warn(`No endpoint found, use default endpoint`); res(DEFAULT_BD_ENDPOINT); return; } log.debug('Loaded BD endpoint successfully'); - res(resp[BACKUP_KEY]); + res(resp[BD_ENDPOINT]); }); }); } From 147081a5bb7583d2f6e3346b77e1dbc134cdc412 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 5 Sep 2020 16:29:26 +0200 Subject: [PATCH 54/81] Clean up make credential --- src/webauthn_authenticator.ts | 29 +++++++----------- src/webauthn_client.ts | 56 ++++++++++++++++++++++++++++------- src/webauthn_psk.ts | 10 +++---- 3 files changed, 61 insertions(+), 34 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 6e1c9e4..28689d4 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -9,16 +9,6 @@ import {PSK} from "./webauthn_psk"; const log = getLogger('webauthn_authenticator'); -export class AttestationObjectWrapper { - public credentialId: string - public rawAttObj: Uint8Array - - constructor(credId: string, raw: Uint8Array) { - this.credentialId = credId; - this.rawAttObj = raw; - } -} - export class AssertionResponse { public authenticatorData: Uint8Array public signature: Uint8Array @@ -157,7 +147,7 @@ export class Authenticator { requireUserVerification: boolean, credTypesAndPubKeyAlgs: PublicKeyCredentialParameters[], excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[], - extensions?: Map): Promise { + extensions?: Map): Promise<[string, Uint8Array]> { log.debug('Called authenticatorMakeCredential'); // Step 2 @@ -173,18 +163,21 @@ export class Authenticator { } // Step 3 - if (excludeCredentialDescriptorList) { + if (excludeCredentialDescriptorList) { // Simplified look up const credMapEntries = await CredentialsMap.load(rpEntity.id); for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); if (credMapEntries.findIndex(x => (x.id == credId) && (x.type === excludeCredentialDescriptorList[i].type)) >= 0) { + await userConsentCallback; throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); } } } + // Step 4 Not needed, because cKey supports resident keys + // Step 5 if (requireUserVerification) { throw new Error(`authenticator does not support user verification`); @@ -195,19 +188,19 @@ export class Authenticator { if (!userConsent) { throw new Error(`no user consent`); } + userEntity.id - return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, undefined, extensions); + return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, undefined, extensions, userEntity.id); } - public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, keyPair?: ICOSECompatibleKey, extensions?: Map): Promise { + public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { // Step 7 if (!(keyPair)) { log.debug('No key pair provided, create new one.'); keyPair = await ECDSA.createECDSAKeyPair(); } let credentialId = this.createCredentialId(); - let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpId); // No - // user Handle + let credentialSource = new PublicKeyCredentialSource(credentialId, keyPair.privateKey, rpId, (userHandle)); await CredentialsMap.put(rpId, credentialSource); // Step 9 @@ -216,7 +209,7 @@ export class Authenticator { log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('Make: PSK requested'); - if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null + if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null in CBOR log.warn('Make: PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); } else { const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); @@ -249,7 +242,7 @@ export class Authenticator { // Return value is not 1:1 WebAuthn conform log.debug('Created credential', credentialId) - return (new AttestationObjectWrapper(credentialId, attObj)); + return [credentialId, attObj]; } private static async generateAttestedCredentialData(credentialId: Uint8Array, publicKey: ICOSECompatibleKey): Promise { diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 67d0329..26e21eb 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -13,26 +13,39 @@ const log = getLogger('webauthn_client'); export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise): Promise { log.debug('Called createPublicKeyCredential'); + // Step 1 + if (!options.publicKey) { + throw new Error('options missing'); + } + // Step 2 if (!sameOriginWithAncestors) { throw new Error(`sameOriginWithAncestors has to be true`); } + // Skip timeout + // Step 7 options.publicKey.rp.id = options.publicKey.rp.id || getDomainFromOrigin(origin); - // Step 11 + // Step 8-10 + const credTypesAndPubKeyAlgs = options.publicKey.pubKeyCredParams; + + // Step 11 + 12 + // Only PSK extension is processed let clientExtensions = undefined; let authenticatorExtensions = undefined; if (options.publicKey.extensions) { const reqExt: any = options.publicKey.extensions; if (reqExt.hasOwnProperty(PSK_EXTENSION_IDENTIFIER)) { - log.debug('PSK extension requested'); + log.info('PSK extension requested'); if (reqExt[PSK_EXTENSION_IDENTIFIER] == true) { log.debug('PSK extension has valid client input'); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; + } else { + log.warn('PSK client extension processing failed. Wrong input.'); } } } @@ -44,7 +57,22 @@ export async function createPublicKeyCredential(origin: string, options: Credent const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); const clientDataHash = new Uint8Array(clientDataHashDigest); - // Step 20: Simplified, just for 1 authenticator + // Handle only 1 authenticator + // Step 20, simplified + if (options.publicKey.authenticatorSelection) { + if (options.publicKey.authenticatorSelection.authenticatorAttachment && (options.publicKey.authenticatorSelection.authenticatorAttachment !== 'platform')) { + throw new Error(`${options.publicKey.authenticatorSelection.authenticatorAttachment} authenticator requested, but only platform authenticators available`); + } + + + // Resident key check can be omitted, because cKey supports resident keys + + if (options.publicKey.authenticatorSelection.userVerification && (options.publicKey.authenticatorSelection.userVerification === 'required')) { + throw new Error(`cKey does not support user verification`); + } + } + + let userVerification = false; let residentKey = false; if (options.publicKey.authenticatorSelection) { @@ -53,25 +81,31 @@ export async function createPublicKeyCredential(origin: string, options: Credent } const userPresence = !userVerification; - const attObjWrapper = await Authenticator.authenticatorMakeCredential(userConsentCallback, + const excludeCredentialDescriptorList = options.publicKey.excludeCredentials // No filtering + + const [credentialId, rawAttObj] = await Authenticator.authenticatorMakeCredential(userConsentCallback, clientDataHash, options.publicKey.rp, options.publicKey.user, residentKey, userPresence, userVerification, - options.publicKey.pubKeyCredParams, - options.publicKey.excludeCredentials, + credTypesAndPubKeyAlgs, + excludeCredentialDescriptorList, authenticatorExtensions); log.debug('Received attestation object'); + if (options.publicKey.attestation === 'none') { // Currently only direct and indirect attestation is supported + throw new Error('Client does not support none attestation'); + } + return { - getClientExtensionResults: () => (clientExtensions), - id: attObjWrapper.credentialId, - rawId: base64ToByteArray(attObjWrapper.credentialId, true), + getClientExtensionResults: () => (clientExtensions), // ToDo Fix client extension output + id: credentialId, + rawId: base64ToByteArray(credentialId, true), response: { - attestationObject: attObjWrapper.rawAttObj.buffer, + attestationObject: rawAttObj.buffer, clientDataJSON: base64ToByteArray(window.btoa(JSON.stringify(clientDataJSON))), }, type: 'public-key', @@ -101,7 +135,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential const customClientDataHash = new Uint8Array(customClientDataHashDigest); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({hash: customClientDataHash})); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: {clientDataJSON: customClientDataJSON}}; // ToDo Add to response + // clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: {clientDataJSON: customClientDataJSON}}; // ToDo Add to response } } } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 2ae9418..861ecc5 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -1,10 +1,10 @@ import * as axios from 'axios'; import * as CBOR from 'cbor'; -import {PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; +import {PSKStorage} from "./webauth_storage"; import {getLogger} from "./logging"; import {base64ToByteArray, byteArrayToBase64} from "./utils"; -import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; +import {ECDSA} from "./webauthn_crypto"; import {getAttestationCertificate} from "./webauthn_attestation"; import {Authenticator} from "./webauthn_authenticator"; import {PSK_EXTENSION_IDENTIFIER} from "./constants"; @@ -175,13 +175,13 @@ export class PSK { keyPair.publicKey = recKey.pubKey; const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - const attObjWrapper = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); + const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); // Finally remove recovery key since PSK output was generated successfully await RecoveryKey.removeRecoveryKey(oldCredentialId); - const recoveryMessage = {attestationObject: attObjWrapper.rawAttObj, oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} + const recoveryMessage = {attestationObject: rawAttObj, oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); - return [attObjWrapper.credentialId, cborRecMsg] + return [credentialId, cborRecMsg] } } \ No newline at end of file From 102fc8197292e8787dc5df940f76be516bdd67fe Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 5 Sep 2020 19:07:48 +0200 Subject: [PATCH 55/81] Clean up get credential --- src/webauthn_authenticator.ts | 6 +++--- src/webauthn_client.ts | 23 ++++++++++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 28689d4..ff5a995 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -50,6 +50,7 @@ export class Authenticator { let isRecovery: [boolean, string] = [false, ""]; let credentialOptions: PublicKeyCredentialSource[] = []; if (allowCredentialDescriptorList) { + // Simplified credential lookup for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); @@ -59,6 +60,7 @@ export class Authenticator { } } } else { + // If no credentials were supplied, load all credentials associated to the RPID credentialOptions = credentialOptions.concat(await CredentialsMap.load(rpId)); } if (credentialOptions.length == 0) { @@ -75,6 +77,7 @@ export class Authenticator { } } if (!isRecovery[0]) { + // No recovery and no associated credential found throw new Error(`Container does not manage any related credentials`); } } @@ -84,7 +87,6 @@ export class Authenticator { credSource = credentialOptions[0]; } - const userConsent = await userConsentCallback; if (!userConsent) { throw new Error(`no user consent`); @@ -93,7 +95,6 @@ export class Authenticator { // Step 8 let processedExtensions = undefined; if (extensions) { - log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('Get: PSK requested'); if (!isRecovery[0]) { @@ -101,7 +102,6 @@ export class Authenticator { } const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); const pskInput = await CBOR.decode(new Buffer(rawPskInput)); - log.debug('Get: PSK input', pskInput); const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery[1], pskInput.hash, rpId); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); credSource = await CredentialsMap.lookup(rpId, newCredId); diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 26e21eb..f241e75 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -113,11 +113,18 @@ export async function createPublicKeyCredential(origin: string, options: Credent } export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise) { + // Step 1 + if (!options.publicKey) { + throw new Error('options missing'); + } + // Step 2 if (!sameOriginWithAncestors) { throw new Error(`sameOriginWithAncestors has to be true`); } + // No timeout + // Step 7 const rpID = options.publicKey.rpId || getDomainFromOrigin(origin); @@ -136,6 +143,8 @@ export async function getPublicKeyCredential(origin: string, options: Credential const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({hash: customClientDataHash})); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); // clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: {clientDataJSON: customClientDataJSON}}; // ToDo Add to response + } else { + log.warn('PSK client extension processing failed. Wrong input.'); } } } @@ -147,21 +156,29 @@ export async function getPublicKeyCredential(origin: string, options: Credential const clientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(clientDataJSON))); const clientDataHash = new Uint8Array(clientDataHashDigest); - // Step 18: Simplified, just for 1 authenticator + // Handle only 1 authenticator + // Step 18 + if (options.publicKey.userVerification && (options.publicKey.userVerification === 'required')) { + throw new Error(`cKey does not support user verification`); + } + const userVerification = options.publicKey.userVerification === "required"; const userPresence = !userVerification; + + const allowCredentialDescriptorList = options.publicKey.allowCredentials; // No filtering + const assertionCreationData = await Authenticator.authenticatorGetAssertion(userConsentCallback, rpID, clientDataHash, userPresence, userVerification, - options.publicKey.allowCredentials, + allowCredentialDescriptorList, authenticatorExtensions); log.debug('Received assertion response'); return { - getClientExtensionResults: () => ({}), + getClientExtensionResults: () => (clientExtensions), // ToDo Add client extension output id: assertionCreationData.credentialId, rawId: base64ToByteArray(assertionCreationData.credentialId, true), response: { From a9b0686b767f33997501ca4b742ec438d61c5070 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 6 Sep 2020 11:14:06 +0200 Subject: [PATCH 56/81] Add auth alias to options --- dist/chromium/options.html | 14 +++++++++++--- src/background.ts | 6 +++--- src/constants.ts | 2 ++ src/options.ts | 4 +++- src/webauth_storage.ts | 38 +++++++++++++++++++++++++++++++++++++- src/webauthn_psk.ts | 13 ++++++++----- 6 files changed, 64 insertions(+), 13 deletions(-) diff --git a/dist/chromium/options.html b/dist/chromium/options.html index 9f2c653..88240d2 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -24,12 +24,20 @@

PSK Options

- +

- Backup Device Contact URL - +
+ Backup Device Contact URL +
+
+ Authenticator Alias +
+
+

+ +

diff --git a/src/background.ts b/src/background.ts index af76b32..8ba618b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -101,9 +101,9 @@ const pskRecovery = async () => { } } -const pskOptions = async (url) => { +const pskOptions = async (alias, url) => { try { - await PSK.setOptions(url); + await PSK.setOptions(alias, url); } catch (e) { log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e); } @@ -124,7 +124,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { pskRecovery().then(() => alert('PSK recovery setup was successfully!'), null); break; case 'psk_options': - pskOptions(msg.url).then(() => alert('PSK options was successfully!'), null); + pskOptions(msg.alias, msg.url).then(() => alert('PSK options was successfully!'), null); break; case 'user_consent': const cb = userConsentCallbacks[msg.tabId]; diff --git a/src/constants.ts b/src/constants.ts index fe2b32c..47e7602 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -27,3 +27,5 @@ export const BACKUP_KEY = 'backup_key'; export const BD_ENDPOINT = 'bd_endpoint'; export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005'; export const RECOVERY_KEY = 'recovery_key'; +export const AUTH_ALIAS = 'auth_alias'; +export const DEFAULT_AUTH_ALIAS = 'MyAuth'; diff --git a/src/options.ts b/src/options.ts index d7b2f1f..e478db0 100644 --- a/src/options.ts +++ b/src/options.ts @@ -10,6 +10,7 @@ $(() => { }); $.when(PSK.bdDeviceUrl()).then((url) => $('#BackupDeviceUrl').val(url)); + $.when(PSK.alias()).then((alias) => $('#AuthenticatorAlias').val(alias)); $('#Recovery').on('click', function(evt: Event) { evt.preventDefault(); @@ -18,11 +19,12 @@ $(() => { }); }); - $('#SaveBackupDeviceUrl').on('click', function(evt: Event) { + $('#SaveOptions').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ type: 'psk_options', url: $('#BackupDeviceUrl').val(), + alias: $('#AuthenticatorAlias').val(), }); }); }); diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 3a9a715..dba202b 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,7 +1,8 @@ import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; import { + AUTH_ALIAS, BACKUP_KEY, - BD_ENDPOINT, + BD_ENDPOINT, DEFAULT_AUTH_ALIAS, DEFAULT_BD_ENDPOINT, ES256, ivLength, keyExportFormat, @@ -50,6 +51,41 @@ export class PSKStorage { }); } + public static async getAlias(): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.get({[AUTH_ALIAS]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.getAlias', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[AUTH_ALIAS] == null) { + log.warn(`No auth alias found, use default alias`); + res(DEFAULT_AUTH_ALIAS); + return; + } + log.debug('Loaded alias successfully'); + res(resp[AUTH_ALIAS]); + }); + }); + } + + public static async setAlias(alias: string): Promise { + log.debug('Set alias to', alias); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[AUTH_ALIAS]: alias}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.setAlias', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } else { + res(); + } + }); + }); + } + public static async storeBackupKeys(backupKeys: BackupKey[], override: boolean = false): Promise { log.debug(`Storing backup keys`); const backupKeysExists = await this.existBackupKeys(); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 861ecc5..f8726cf 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -66,13 +66,17 @@ export class PSK { return await PSKStorage.getBDEndpoint(); } - public static async setOptions(url: string): Promise { - return await PSKStorage.setBDEndpoint(url); + public static async setOptions(alias: string, url: string): Promise<[void, void]> { + return await Promise.all([PSKStorage.setAlias(alias), PSKStorage.setBDEndpoint(url)]); + } + + public static async alias(): Promise { + return await PSKStorage.getAlias(); } public static async setup(): Promise { const bdEndpoint = await PSKStorage.getBDEndpoint(); - const authAlias = prompt('Please enter an alias name for your authenticator', 'MyAuth'); + const authAlias = await this.alias(); const keyAmount: number = +prompt('How many backup keys should be created?', '5'); return await axios.default.post(bdEndpoint + '/setup', {authAlias, keyAmount}) @@ -91,9 +95,8 @@ export class PSK { } public static async recoverySetup(): Promise { - const authAlias = prompt('Which authenticator you want to recover?', 'OldAuth'); - const newAuthAlias = prompt('What is alias of your current authenticator?', 'MyAuth'); + const newAuthAlias = await this.alias(); const bdEndpoint = await PSKStorage.getBDEndpoint(); return await axios.default.get(bdEndpoint + '/recovery?authAlias=' + authAlias) From f45f56cb4d72fd7dacaaba47e009749e99235edb Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 6 Sep 2020 18:58:04 +0200 Subject: [PATCH 57/81] Encode attestation object during recovery in base64 URL --- src/webauthn_authenticator.ts | 1 + src/webauthn_psk.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index ff5a995..3dfdb8b 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -252,6 +252,7 @@ export class Authenticator { credIdLen[1] = credentialId.length & 0xff; const coseKey = await publicKey.toCOSE(publicKey.publicKey); const encodedKey = new Uint8Array(CBOR.encodeCanonical(coseKey)); + log.debug('New pub key', byteArrayToBase64(encodedKey, true)); const attestedCredentialDataLength = aaguid.length + credIdLen.length + credentialId.length + encodedKey.length; const attestedCredentialData = new Uint8Array(attestedCredentialDataLength); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index f8726cf..6651fa5 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -180,10 +180,13 @@ export class PSK { const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); + log.debug('Delegation signature', recKey.delegationSignature); + log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); + // Finally remove recovery key since PSK output was generated successfully await RecoveryKey.removeRecoveryKey(oldCredentialId); - const recoveryMessage = {attestationObject: rawAttObj, oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} + const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); return [credentialId, cborRecMsg] } From f39367842c64264a2e8ed885e79a41d311f68a62 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 11 Sep 2020 15:06:48 +0200 Subject: [PATCH 58/81] Moved user interaction from authenticator to BD --- dist/chromium/options.html | 6 +---- src/background.ts | 31 +++++--------------------- src/constants.ts | 1 + src/options.ts | 13 ++--------- src/webauth_storage.ts | 35 ----------------------------- src/webauthn_psk.ts | 45 ++++++++++++++++++++------------------ 6 files changed, 34 insertions(+), 97 deletions(-) diff --git a/dist/chromium/options.html b/dist/chromium/options.html index 88240d2..f7984c7 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -24,16 +24,12 @@

PSK Options

- - +

Backup Device Contact URL
-
- Authenticator Alias -

diff --git a/src/background.ts b/src/background.ts index 8ba618b..d5b6f5b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -85,28 +85,12 @@ const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { } }; -const pskSetup = async () => { - try { - await PSK.setup(); - } catch (e) { - log.error('failed to setup psk', { errorType: `${(typeof e)}` }, e); - } +const pskSync = async () => { + await PSK.sync(); }; -const pskRecovery = async () => { - try { - await PSK.recoverySetup(); - } catch (e) { - log.error('failed to setup psk recovery', { errorType: `${(typeof e)}` }, e); - } -} - const pskOptions = async (alias, url) => { - try { - await PSK.setOptions(alias, url); - } catch (e) { - log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e); - } + await PSK.setOptions(alias, url); }; chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { @@ -117,14 +101,11 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'get_credential': getCredential(msg, sender).then(sendResponse); break; - case 'psk_setup': - pskSetup().then(() => alert('PSK setup was successfully!'), null); - break; - case 'psk_recovery': - pskRecovery().then(() => alert('PSK recovery setup was successfully!'), null); + case 'psk_sync': + pskSync().then(() => alert('PSK sync was successfully!'), e => log.error('failed to sync psk', { errorType: `${(typeof e)}` }, e)); break; case 'psk_options': - pskOptions(msg.alias, msg.url).then(() => alert('PSK options was successfully!'), null); + pskOptions(msg.alias, msg.url).then(() => alert('PSK options was successfully!'), e => log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e)); break; case 'user_consent': const cb = userConsentCallbacks[msg.tabId]; diff --git a/src/constants.ts b/src/constants.ts index 47e7602..5954740 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -29,3 +29,4 @@ export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005'; export const RECOVERY_KEY = 'recovery_key'; export const AUTH_ALIAS = 'auth_alias'; export const DEFAULT_AUTH_ALIAS = 'MyAuth'; +export const BD_TIMEOUT = 60 * 1000 * 10; // 10 minutes diff --git a/src/options.ts b/src/options.ts index e478db0..5a1e6ed 100644 --- a/src/options.ts +++ b/src/options.ts @@ -2,20 +2,12 @@ import $ from 'jquery'; import {PSK} from "./webauthn_psk"; $(() => { - $('#Setup').on('click', function(evt: Event) { - evt.preventDefault(); - chrome.runtime.sendMessage({ - type: 'psk_setup', - }); - }); - $.when(PSK.bdDeviceUrl()).then((url) => $('#BackupDeviceUrl').val(url)); - $.when(PSK.alias()).then((alias) => $('#AuthenticatorAlias').val(alias)); - $('#Recovery').on('click', function(evt: Event) { + $('#Sync').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'psk_recovery', + type: 'psk_sync', }); }); @@ -24,7 +16,6 @@ $(() => { chrome.runtime.sendMessage({ type: 'psk_options', url: $('#BackupDeviceUrl').val(), - alias: $('#AuthenticatorAlias').val(), }); }); }); diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index dba202b..29b1e58 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -51,41 +51,6 @@ export class PSKStorage { }); } - public static async getAlias(): Promise { - return new Promise(async (res, rej) => { - chrome.storage.local.get({[AUTH_ALIAS]: null}, async (resp) => { - if (!!chrome.runtime.lastError) { - log.error('Could not perform PSKStorage.getAlias', chrome.runtime.lastError.message); - rej(chrome.runtime.lastError); - return; - } - - if (resp[AUTH_ALIAS] == null) { - log.warn(`No auth alias found, use default alias`); - res(DEFAULT_AUTH_ALIAS); - return; - } - log.debug('Loaded alias successfully'); - res(resp[AUTH_ALIAS]); - }); - }); - } - - public static async setAlias(alias: string): Promise { - log.debug('Set alias to', alias); - return new Promise(async (res, rej) => { - chrome.storage.local.set({[AUTH_ALIAS]: alias}, () => { - if (!!chrome.runtime.lastError) { - log.error('Could not perform PSKStorage.setAlias', chrome.runtime.lastError.message); - rej(chrome.runtime.lastError); - return; - } else { - res(); - } - }); - }); - } - public static async storeBackupKeys(backupKeys: BackupKey[], override: boolean = false): Promise { log.debug(`Storing backup keys`); const backupKeysExists = await this.existBackupKeys(); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 6651fa5..b1d7d84 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -7,7 +7,7 @@ import {base64ToByteArray, byteArrayToBase64} from "./utils"; import {ECDSA} from "./webauthn_crypto"; import {getAttestationCertificate} from "./webauthn_attestation"; import {Authenticator} from "./webauthn_authenticator"; -import {PSK_EXTENSION_IDENTIFIER} from "./constants"; +import {BD_TIMEOUT, PSK_EXTENSION_IDENTIFIER} from "./constants"; const log = getLogger('webauthn_psk'); @@ -66,23 +66,20 @@ export class PSK { return await PSKStorage.getBDEndpoint(); } - public static async setOptions(alias: string, url: string): Promise<[void, void]> { - return await Promise.all([PSKStorage.setAlias(alias), PSKStorage.setBDEndpoint(url)]); + public static async setOptions(alias: string, url: string): Promise { + return await PSKStorage.setBDEndpoint(url); } - public static async alias(): Promise { - return await PSKStorage.getAlias(); - } + public static async sync(): Promise { + log.debug('Sync triggered'); - public static async setup(): Promise { const bdEndpoint = await PSKStorage.getBDEndpoint(); - const authAlias = await this.alias(); - const keyAmount: number = +prompt('How many backup keys should be created?', '5'); - return await axios.default.post(bdEndpoint + '/setup', {authAlias, keyAmount}) + return await axios.default.get(bdEndpoint + '/sync', {timeout: BD_TIMEOUT}) .then(async function(response) { log.debug(response); - const setupResponse = response.data; + const syncResponse = response.data; + const setupResponse = syncResponse.setup; const backupKeys = new Array(); for (let i = 0; i < setupResponse.length; ++i) { const backupKey = new BackupKey(setupResponse[i].credId, setupResponse[i].attObj); @@ -91,18 +88,23 @@ export class PSK { log.debug('Loaded backup keys', backupKeys); await PSKStorage.storeBackupKeys(backupKeys); + + if (syncResponse.recoveryRequired) { + await PSK.recoverySync(syncResponse.authAlias); + } }); } - public static async recoverySetup(): Promise { - const authAlias = prompt('Which authenticator you want to recover?', 'OldAuth'); - const newAuthAlias = await this.alias(); + private static async recoverySync(newAuthAlias: string): Promise { + log.debug("Recovery setup triggered"); + const bdEndpoint = await PSKStorage.getBDEndpoint(); - return await axios.default.get(bdEndpoint + '/recovery?authAlias=' + authAlias) + return await axios.default.get(bdEndpoint + '/recovery', {timeout: BD_TIMEOUT}) .then(async function(initResponse) { - log.debug(initResponse); - const keyAmount = initResponse.data.keyAmount; + const initRecResponse = initResponse.data; + const keyAmount = initRecResponse.keyAmount; + const replacementAuthAlias = initRecResponse.replacementAuthAlias; let rawRecKeys = new Array<[string, CryptoKeyPair]>() let replacementKeys = [] @@ -123,11 +125,12 @@ export class PSK { let attCert = byteArrayToBase64(getAttestationCertificate(), true); - await axios.default.post(bdEndpoint + '/recovery?authAlias=' + authAlias, { + await axios.default.post(bdEndpoint + '/recovery', { repKeys: replacementKeys, attCert, - newAuthAlias - }) + newAuthAlias, + replacementAuthAlias + }, {timeout: BD_TIMEOUT}) .then(async function (delResponse) { const rawDelegations = delResponse.data; @@ -154,7 +157,7 @@ export class PSK { recoveryKeys.push(recoveryKey); } - log.debug('Received recovery keys', recoveryKeys); + log.debug('Recovery finished. Recovery keys:', recoveryKeys); await PSKStorage.storeRecoveryKeys(recoveryKeys); }); }); From 0941731bfe4ba8725d9a07941cd7de36bc82fae9 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 14 Sep 2020 08:48:45 +0200 Subject: [PATCH 59/81] Adapt Sync messages --- src/webauthn_psk.ts | 106 ++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 57 deletions(-) diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index b1d7d84..5272116 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -79,87 +79,79 @@ export class PSK { .then(async function(response) { log.debug(response); const syncResponse = response.data; - const setupResponse = syncResponse.setup; const backupKeys = new Array(); - for (let i = 0; i < setupResponse.length; ++i) { - const backupKey = new BackupKey(setupResponse[i].credId, setupResponse[i].attObj); + for (let i = 0; i < syncResponse.backupPublicKeys.length; ++i) { + const backupKey = new BackupKey(syncResponse.backupPublicKeys[i].credId, syncResponse.backupPublicKeys[i].attObj); backupKeys.push(backupKey); } - log.debug('Loaded backup keys', backupKeys); + log.debug('Setup finished. Backup keys', backupKeys); await PSKStorage.storeBackupKeys(backupKeys); - if (syncResponse.recoveryRequired) { - await PSK.recoverySync(syncResponse.authAlias); + if (syncResponse.hasOwnProperty("recoveryOption")) { + await PSK.recoverySync(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); } }); } - private static async recoverySync(newAuthAlias: string): Promise { + private static async recoverySync(delegatedAuthAlias: string, originAuthAlias: string, keyAmount: number): Promise { log.debug("Recovery setup triggered"); const bdEndpoint = await PSKStorage.getBDEndpoint(); - return await axios.default.get(bdEndpoint + '/recovery', {timeout: BD_TIMEOUT}) - .then(async function(initResponse) { - const initRecResponse = initResponse.data; - const keyAmount = initRecResponse.keyAmount; - const replacementAuthAlias = initRecResponse.replacementAuthAlias; - - let rawRecKeys = new Array<[string, CryptoKeyPair]>() - let replacementKeys = [] - for (let i = 0; i < keyAmount; i++) { - const keyPair = await window.crypto.subtle.generateKey( - {name: 'ECDSA', namedCurve: 'P-256'}, - true, - ['sign'], - ); - rawRecKeys.push([i.toString(), keyPair]); - - // Prepare delegation request - const pubKey = await ECDSA.fromKey(keyPair.publicKey); - const cosePubKey = await pubKey.toCOSE(pubKey.publicKey); - const encodedPubKey = new Uint8Array(CBOR.encodeCanonical(cosePubKey)); - replacementKeys.push({keyId: i.toString(), replacementPubKey: byteArrayToBase64(encodedPubKey, true)}); - } + let rawRecKeys = new Array<[string, CryptoKeyPair]>() + let replacementKeys = [] + for (let i = 0; i < keyAmount; i++) { + const keyPair = await window.crypto.subtle.generateKey( + {name: 'ECDSA', namedCurve: 'P-256'}, + true, + ['sign'], + ); + rawRecKeys.push([i.toString(), keyPair]); + + // Prepare delegation request + const pubKey = await ECDSA.fromKey(keyPair.publicKey); + const cosePubKey = await pubKey.toCOSE(pubKey.publicKey); + const encodedPubKey = new Uint8Array(CBOR.encodeCanonical(cosePubKey)); + replacementKeys.push({keyId: i.toString(), pubKey: byteArrayToBase64(encodedPubKey, true)}); + } - let attCert = byteArrayToBase64(getAttestationCertificate(), true); + let attCert = byteArrayToBase64(getAttestationCertificate(), true); - await axios.default.post(bdEndpoint + '/recovery', { - repKeys: replacementKeys, - attCert, - newAuthAlias, - replacementAuthAlias - }, {timeout: BD_TIMEOUT}) - .then(async function (delResponse) { - const rawDelegations = delResponse.data; + return await axios.default.post(bdEndpoint + '/recovery', { + replacementKeys, + attCert, + delegatedAuthAlias, + originAuthAlias + }, {timeout: BD_TIMEOUT}) + .then(async function (delResponse) { + const rawDelegations = delResponse.data.delegations; - let recoveryKeys = new Array() + let recoveryKeys = new Array() - for (let i = 0; i < rawDelegations.length; ++i) { - const sign = rawDelegations[i].sign; - const credId = rawDelegations[i].credId; - const keyId = rawDelegations[i].keyId; + for (let i = 0; i < rawDelegations.length; ++i) { + const sign = rawDelegations[i].sign; + const credId = rawDelegations[i].credId; + const keyId = rawDelegations[i].keyId; - log.debug(rawDelegations[i]); + log.debug(rawDelegations[i]); - const keyPair = rawRecKeys.filter((x, _) => x[0] == keyId); - if (keyPair.length !== 1) { - log.warn('BD response does not contain delegation for key pair', keyId); - continue; - } + const keyPair = rawRecKeys.filter((x, _) => x[0] == keyId); + if (keyPair.length !== 1) { + log.warn('BD response does not contain delegation for key pair', keyId); + continue; + } - const pubKey = keyPair[0][1].publicKey; - const privKey = keyPair[0][1].privateKey; + const pubKey = keyPair[0][1].publicKey; + const privKey = keyPair[0][1].privateKey; - const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign) + const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign) - recoveryKeys.push(recoveryKey); - } + recoveryKeys.push(recoveryKey); + } - log.debug('Recovery finished. Recovery keys:', recoveryKeys); - await PSKStorage.storeRecoveryKeys(recoveryKeys); - }); + log.debug('Recovery finished. Recovery keys:', recoveryKeys); + await PSKStorage.storeRecoveryKeys(recoveryKeys); }); } From f6f088956eb0a51777b689959b27f402cc1b462f Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 17 Sep 2020 14:31:15 +0200 Subject: [PATCH 60/81] Remove redundant CBOR encapsulation --- src/webauthn_authenticator.ts | 4 +++- src/webauthn_client.ts | 10 ++-------- src/webauthn_psk.ts | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 3dfdb8b..4f3dbd3 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -209,7 +209,9 @@ export class Authenticator { log.debug(extensions); if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('Make: PSK requested'); - if (extensions.get(PSK_EXTENSION_IDENTIFIER) !== "9g") { // null in CBOR + const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); + const pskInput = await CBOR.decode(new Buffer(rawPskInput)); + if (pskInput !== true) { log.warn('Make: PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); } else { const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index f241e75..8952ae5 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -39,14 +39,8 @@ export async function createPublicKeyCredential(origin: string, options: Credent const reqExt: any = options.publicKey.extensions; if (reqExt.hasOwnProperty(PSK_EXTENSION_IDENTIFIER)) { log.info('PSK extension requested'); - if (reqExt[PSK_EXTENSION_IDENTIFIER] == true) { - log.debug('PSK extension has valid client input'); - const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); - authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; - } else { - log.warn('PSK client extension processing failed. Wrong input.'); - } + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); + authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); } } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 5272116..afe0e58 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -157,7 +157,7 @@ export class PSK { public static async authenticatorMakeCredentialExtensionOutput(): Promise<[string, Uint8Array]> { const backupKey = await BackupKey.popBackupKey(); - return [backupKey.credentialId, CBOR.encodeCanonical({bckpDvcAttObj: base64ToByteArray(backupKey.bdAttObj, true)})]; + return [backupKey.credentialId, base64ToByteArray(backupKey.bdAttObj, true)]; } public static async authenticatorGetCredentialExtensionOutput(oldCredentialId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { @@ -171,7 +171,7 @@ export class PSK { // Create attestation object using the key pair of the recovery key + request PSK extension const keyPair = await ECDSA.fromKey(recKey.privKey); keyPair.publicKey = recKey.pubKey; - const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(null)); + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); From 4fec8cc9b9034872d5c4603cdb6fb3c020eb03f5 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 30 Sep 2020 20:03:33 +0200 Subject: [PATCH 61/81] Add BD authData to recovery key --- src/webauth_storage.ts | 3 ++- src/webauthn_psk.ts | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 29b1e58..266dfbd 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -126,6 +126,7 @@ export class PSKStorage { pubKey: expPubKey, privKey: expPrvKey, delegationSignature: recKey.delegationSignature, + bdAuthData: recKey.bdAuthData, } exportKeys.push(json) @@ -182,7 +183,7 @@ export class PSKStorage { [], ); - const recKey = new RecoveryKey(json.credentialId, pubKey, prvKey, json.delegationSignature); + const recKey = new RecoveryKey(json.credentialId, pubKey, prvKey, json.delegationSignature, json.bdAuthData); recKeys.push(recKey); } log.debug('Loaded recovery keys successfully', recKeys); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index afe0e58..ca5940f 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -38,12 +38,14 @@ export class RecoveryKey { public pubKey: CryptoKey public privKey: CryptoKey public delegationSignature: string + public bdAuthData: string - constructor(credId: string, pubKey: CryptoKey, privKey: CryptoKey, sign: string) { + constructor(credId: string, pubKey: CryptoKey, privKey: CryptoKey, sign: string, authdata: string) { this.credentialId = credId; this.pubKey = pubKey; this.privKey = privKey; this.delegationSignature = sign; + this.bdAuthData = authdata; } static async findRecoveryKey(credId: string): Promise { @@ -133,6 +135,7 @@ export class PSK { const sign = rawDelegations[i].sign; const credId = rawDelegations[i].credId; const keyId = rawDelegations[i].keyId; + const authData = rawDelegations[i].authData; log.debug(rawDelegations[i]); @@ -145,7 +148,7 @@ export class PSK { const pubKey = keyPair[0][1].publicKey; const privKey = keyPair[0][1].privateKey; - const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign) + const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign, authData) recoveryKeys.push(recoveryKey); } @@ -181,7 +184,7 @@ export class PSK { // Finally remove recovery key since PSK output was generated successfully await RecoveryKey.removeRecoveryKey(oldCredentialId); - const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature} + const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature, bdAuthData: recKey.bdAuthData} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); return [credentialId, cborRecMsg] } From bde897374a1fc1f6637c64d5f43de4b1c641b6e5 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 1 Oct 2020 20:08:01 +0200 Subject: [PATCH 62/81] Add BD recovery --- src/constants.ts | 1 + src/webauth_storage.ts | 82 +++++++++++++++++++++++++++-------- src/webauthn_authenticator.ts | 5 +-- src/webauthn_psk.ts | 82 ++++++++++++++++++++--------------- 4 files changed, 115 insertions(+), 55 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 5954740..1c56acf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,3 +30,4 @@ export const RECOVERY_KEY = 'recovery_key'; export const AUTH_ALIAS = 'auth_alias'; export const DEFAULT_AUTH_ALIAS = 'MyAuth'; export const BD_TIMEOUT = 60 * 1000 * 10; // 10 minutes +export const BD = 'bd' diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 266dfbd..6aa209d 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,8 +1,7 @@ import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; import { - AUTH_ALIAS, - BACKUP_KEY, - BD_ENDPOINT, DEFAULT_AUTH_ALIAS, + BACKUP_KEY, BD, + BD_ENDPOINT, DEFAULT_BD_ENDPOINT, ES256, ivLength, keyExportFormat, @@ -51,18 +50,64 @@ export class PSKStorage { }); } - public static async storeBackupKeys(backupKeys: BackupKey[], override: boolean = false): Promise { - log.debug(`Storing backup keys`); + public static async storeBD(bdUUID: string): Promise { + log.debug('Store BD'); + let bds = await this.loadBDs(); + if (bds.includes(bdUUID)) { + return; + } else { + bds = bds.concat(bdUUID); + const exportJSON = JSON.stringify(bds); + return new Promise(async (res, rej) => { + chrome.storage.local.set({[BD]: exportJSON}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.storeBD', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } else { + res(); + } + }); + }); + } + + } + + public static async loadBDs(): Promise> { + log.debug(`Loading BDs`); + return new Promise>(async (res, rej) => { + chrome.storage.local.get({[BD]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.loadBDs', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[BD] == null) { + log.warn(`No BDs found`); + res([]); + return; + } + + const bds = await JSON.parse(resp[BD]); + log.debug('Loaded BDs successfully'); + res(bds); + }); + }); + } + + public static async storeBackupKeys(backupKeys: BackupKey[], bdUUID: string, override: boolean = false): Promise { + log.debug(`Storing backup keys for`, bdUUID); const backupKeysExists = await this.existBackupKeys(); if (backupKeysExists && !override) { log.debug('Backup keys already exist. Update entry.'); - const entries = await this.loadBackupKeys(); + const entries = await this.loadBackupKeys(bdUUID); backupKeys = entries.concat(backupKeys); } let exportJSON = JSON.stringify(backupKeys); return new Promise(async (res, rej) => { - chrome.storage.local.set({[BACKUP_KEY]: exportJSON}, () => { + chrome.storage.local.set({[BACKUP_KEY + '_' + bdUUID]: exportJSON}, () => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.storeBackupKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); @@ -74,23 +119,23 @@ export class PSKStorage { }); }; - public static async loadBackupKeys(): Promise { + public static async loadBackupKeys(bdUUID: string): Promise { log.debug(`Loading backup keys`); return new Promise(async (res, rej) => { - chrome.storage.local.get({[BACKUP_KEY]: null}, async (resp) => { + chrome.storage.local.get({[BACKUP_KEY + '_' + bdUUID]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.loadBackupKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); return; } - if (resp[BACKUP_KEY] == null) { + if (resp[BACKUP_KEY + '_' + bdUUID] == null) { log.warn(`No backup keys found`); res([]); return; } - const backupKeys = await JSON.parse(resp[BACKUP_KEY]); + const backupKeys = await JSON.parse(resp[BACKUP_KEY + '_' + bdUUID]); log.debug('Loaded backup keys successfully'); res(backupKeys); }); @@ -114,6 +159,8 @@ export class PSKStorage { public static async storeRecoveryKeys(recoveryKeys: RecoveryKey[]): Promise { log.debug('Storing recovery keys'); + recoveryKeys = recoveryKeys.concat(await this.loadRecoveryKeys()); + // Export recoveryKeys const exportKeys = [] for (let i = 0; i < recoveryKeys.length; i++) { @@ -122,11 +169,11 @@ export class PSKStorage { const expPubKey = await window.crypto.subtle.exportKey('jwk', recKey.pubKey); const json = { - credentialId: recKey.credentialId, + backupKeyId: recKey.backupKeyId, pubKey: expPubKey, privKey: expPrvKey, delegationSignature: recKey.delegationSignature, - bdAuthData: recKey.bdAuthData, + bdData: recKey.bdData, } exportKeys.push(json) @@ -146,9 +193,10 @@ export class PSKStorage { }); } - public static async recoveryKeyExists(credId: string): Promise { - const backupKeys = await PSKStorage.loadRecoveryKeys(); - return backupKeys.filter(x => x.credentialId === credId).length > 0 + public static async recoveryKeyExists(backupKeyId: string): Promise { + log.debug('recoveryKeyExists: Requested backup key ID', backupKeyId); + const recoveryKeys = await PSKStorage.loadRecoveryKeys(); + return recoveryKeys.filter(x => x.backupKeyId === backupKeyId).length > 0 } public static async loadRecoveryKeys(): Promise { @@ -183,7 +231,7 @@ export class PSKStorage { [], ); - const recKey = new RecoveryKey(json.credentialId, pubKey, prvKey, json.delegationSignature, json.bdAuthData); + const recKey = new RecoveryKey(json.backupKeyId, pubKey, prvKey, json.delegationSignature, json.bdData); recKeys.push(recKey); } log.debug('Loaded recovery keys successfully', recKeys); diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 4f3dbd3..9462a3a 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -214,11 +214,8 @@ export class Authenticator { if (pskInput !== true) { log.warn('Make: PSK extension received unexpected input. Skip extension processing.', extensions[PSK_EXTENSION_IDENTIFIER]); } else { - const [backupKeyCredentialId, pskOutPut] = await PSK.authenticatorMakeCredentialExtensionOutput(); + const pskOutPut = await PSK.authenticatorMakeCredentialExtensionOutput(); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutPut]]); - credentialId = backupKeyCredentialId; - credentialSource.id = credentialId; - await CredentialsMap.put(rpId, credentialSource); log.debug('Make: Processed PSK'); } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index ca5940f..430dc01 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -12,44 +12,53 @@ import {BD_TIMEOUT, PSK_EXTENSION_IDENTIFIER} from "./constants"; const log = getLogger('webauthn_psk'); export class BackupKey { - public credentialId: string; public bdAttObj: string; // base64 URL with padding - constructor(credId: string, attObj: string) { - this.credentialId = credId; + constructor(attObj: string) { this.bdAttObj = attObj; } - static async popBackupKey(): Promise { - const backupKeys = await PSKStorage.loadBackupKeys(); + static async popBackupKeys(): Promise { + const bds = await PSKStorage.loadBDs(); + const backupKeys = Array(); + + for (let i = 0; i < bds.length; i++) { + const bdBackupKeys = await PSKStorage.loadBackupKeys(bds[i]); + if (bdBackupKeys.length == 0) { + log.warn('No backup keys for ' + bds[i]); + continue; + } + const backupKey = bdBackupKeys.pop(); + await PSKStorage.storeBackupKeys(bdBackupKeys, bds[i], true); + backupKeys.push(backupKey); + } if (backupKeys.length == 0) { throw new Error('No backup keys available'); } - const backupKey = backupKeys.pop(); - log.debug('Pop backup key', backupKey); - await PSKStorage.storeBackupKeys(backupKeys, true); - return backupKey; + log.debug('Pop backup keys: ', backupKeys) + + return backupKeys; } } export class RecoveryKey { - public credentialId: string + public backupKeyId: string public pubKey: CryptoKey public privKey: CryptoKey public delegationSignature: string - public bdAuthData: string + public bdData: string - constructor(credId: string, pubKey: CryptoKey, privKey: CryptoKey, sign: string, authdata: string) { - this.credentialId = credId; + constructor(backupKeyId: string, pubKey: CryptoKey, privKey: CryptoKey, sign: string, bdData: string) { + this.backupKeyId = backupKeyId; this.pubKey = pubKey; this.privKey = privKey; this.delegationSignature = sign; - this.bdAuthData = authdata; + this.bdData = bdData; } - static async findRecoveryKey(credId: string): Promise { - const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId === credId); + static async findRecoveryKey(backupKeyId: string): Promise { + const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.backupKeyId === backupKeyId); if (recoveryKeys.length == 0) { return null } @@ -57,8 +66,8 @@ export class RecoveryKey { return recoveryKeys[0]; } - static async removeRecoveryKey(credId: string): Promise { - const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.credentialId !== credId); + static async removeRecoveryKey(backupKeyId: string): Promise { + const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.backupKeyId !== backupKeyId); return await PSKStorage.storeRecoveryKeys(recoveryKeys); } } @@ -83,12 +92,13 @@ export class PSK { const syncResponse = response.data; const backupKeys = new Array(); for (let i = 0; i < syncResponse.backupPublicKeys.length; ++i) { - const backupKey = new BackupKey(syncResponse.backupPublicKeys[i].credId, syncResponse.backupPublicKeys[i].attObj); + const backupKey = new BackupKey(syncResponse.backupPublicKeys[i].attObj); backupKeys.push(backupKey); } log.debug('Setup finished. Backup keys', backupKeys); - await PSKStorage.storeBackupKeys(backupKeys); + await PSKStorage.storeBD(syncResponse.bdUUID); + await PSKStorage.storeBackupKeys(backupKeys, syncResponse.bdUUID); if (syncResponse.hasOwnProperty("recoveryOption")) { await PSK.recoverySync(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); @@ -115,7 +125,7 @@ export class PSK { const pubKey = await ECDSA.fromKey(keyPair.publicKey); const cosePubKey = await pubKey.toCOSE(pubKey.publicKey); const encodedPubKey = new Uint8Array(CBOR.encodeCanonical(cosePubKey)); - replacementKeys.push({keyId: i.toString(), pubKey: byteArrayToBase64(encodedPubKey, true)}); + replacementKeys.push({replacementKeyId: i.toString(), pubKey: byteArrayToBase64(encodedPubKey, true)}); } let attCert = byteArrayToBase64(getAttestationCertificate(), true); @@ -133,22 +143,22 @@ export class PSK { for (let i = 0; i < rawDelegations.length; ++i) { const sign = rawDelegations[i].sign; - const credId = rawDelegations[i].credId; - const keyId = rawDelegations[i].keyId; - const authData = rawDelegations[i].authData; + const backupKeyId = rawDelegations[i].backupKeyId; + const replacementKeyId = rawDelegations[i].replacementKeyId; + const bdData = rawDelegations[i].bdData; log.debug(rawDelegations[i]); - const keyPair = rawRecKeys.filter((x, _) => x[0] == keyId); + const keyPair = rawRecKeys.filter((x, _) => x[0] == replacementKeyId); if (keyPair.length !== 1) { - log.warn('BD response does not contain delegation for key pair', keyId); + log.warn('BD response does not contain delegation for key pair', replacementKeyId); continue; } const pubKey = keyPair[0][1].publicKey; const privKey = keyPair[0][1].privateKey; - const recoveryKey = new RecoveryKey(credId, pubKey, privKey, sign, authData) + const recoveryKey = new RecoveryKey(backupKeyId, pubKey, privKey, sign, bdData) recoveryKeys.push(recoveryKey); } @@ -158,15 +168,19 @@ export class PSK { }); } - public static async authenticatorMakeCredentialExtensionOutput(): Promise<[string, Uint8Array]> { - const backupKey = await BackupKey.popBackupKey(); - return [backupKey.credentialId, base64ToByteArray(backupKey.bdAttObj, true)]; + public static async authenticatorMakeCredentialExtensionOutput(): Promise { + const backupKeys = await BackupKey.popBackupKeys(); + const raw_backup_keys = Array(); + for (let i = 0; i < backupKeys.length; i++) { + raw_backup_keys.push(base64ToByteArray(backupKeys[i].bdAttObj, true)); + } + return raw_backup_keys; } - public static async authenticatorGetCredentialExtensionOutput(oldCredentialId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { + public static async authenticatorGetCredentialExtensionOutput(oldBackupKeyId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { log.debug('authenticatorGetCredentialExtensionOutput called'); // Find recovery key for given credential id - const recKey = await RecoveryKey.findRecoveryKey(oldCredentialId); + const recKey = await RecoveryKey.findRecoveryKey(oldBackupKeyId); if (recKey == null) { throw new Error("No recovery key found, but recovery was detected"); } @@ -182,9 +196,9 @@ export class PSK { log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); // Finally remove recovery key since PSK output was generated successfully - await RecoveryKey.removeRecoveryKey(oldCredentialId); + await RecoveryKey.removeRecoveryKey(oldBackupKeyId); - const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldCredentialId: oldCredentialId, delegationSignature: recKey.delegationSignature, bdAuthData: recKey.bdAuthData} + const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldBackupKeyId: oldBackupKeyId, delegationSignature: recKey.delegationSignature, bdData: recKey.bdData} const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); return [credentialId, cborRecMsg] } From 6e6a4fff70a8a0c4e7be0b3f911fada9d23bdb90 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 5 Oct 2020 21:52:56 +0200 Subject: [PATCH 63/81] Remove unnecessary CBOR encoding in PSK Authentication Extension --- src/webauthn_authenticator.ts | 2 +- src/webauthn_client.ts | 3 +-- src/webauthn_psk.ts | 5 ++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 9462a3a..e26dff4 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -102,7 +102,7 @@ export class Authenticator { } const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); const pskInput = await CBOR.decode(new Buffer(rawPskInput)); - const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery[1], pskInput.hash, rpId); + const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery[1], pskInput, rpId); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); credSource = await CredentialsMap.lookup(rpId, newCredId); if (credSource == null) { diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 8952ae5..8b80c04 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -134,9 +134,8 @@ export async function getPublicKeyCredential(origin: string, options: Credential const customClientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); const customClientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(customClientDataJSON))); const customClientDataHash = new Uint8Array(customClientDataHashDigest); - const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({hash: customClientDataHash})); + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(customClientDataHash)); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - // clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: {clientDataJSON: customClientDataJSON}}; // ToDo Add to response } else { log.warn('PSK client extension processing failed. Wrong input.'); } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 430dc01..23e3596 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -177,7 +177,7 @@ export class PSK { return raw_backup_keys; } - public static async authenticatorGetCredentialExtensionOutput(oldBackupKeyId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, Uint8Array]> { + public static async authenticatorGetCredentialExtensionOutput(oldBackupKeyId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, any]> { log.debug('authenticatorGetCredentialExtensionOutput called'); // Find recovery key for given credential id const recKey = await RecoveryKey.findRecoveryKey(oldBackupKeyId); @@ -199,7 +199,6 @@ export class PSK { await RecoveryKey.removeRecoveryKey(oldBackupKeyId); const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldBackupKeyId: oldBackupKeyId, delegationSignature: recKey.delegationSignature, bdData: recKey.bdData} - const cborRecMsg = new Uint8Array(CBOR.encodeCanonical(recoveryMessage)); - return [credentialId, cborRecMsg] + return [credentialId, recoveryMessage] } } \ No newline at end of file From c71d62d8063b196d6c8573d354155643e9a2eae7 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 7 Oct 2020 16:17:55 +0200 Subject: [PATCH 64/81] Abort registration if one BD has no backup keys --- src/webauthn_psk.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 23e3596..28455da 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -25,8 +25,7 @@ export class BackupKey { for (let i = 0; i < bds.length; i++) { const bdBackupKeys = await PSKStorage.loadBackupKeys(bds[i]); if (bdBackupKeys.length == 0) { - log.warn('No backup keys for ' + bds[i]); - continue; + throw new Error('No backup keys available for ' + bds[i]); } const backupKey = bdBackupKeys.pop(); await PSKStorage.storeBackupKeys(bdBackupKeys, bds[i], true); From 91b93fb36d16b4deed50e155fa6034e68bd13b7f Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 7 Oct 2020 20:48:22 +0200 Subject: [PATCH 65/81] Add user verification --- dist/chromium/options.html | 1 + src/background.ts | 16 +++++++++++ src/constants.ts | 3 +-- src/options.ts | 7 +++++ src/webauth_storage.ts | 50 ++++++++++++++++++++++++++++++----- src/webauthn_authenticator.ts | 45 ++++++++++++++++++++++--------- src/webauthn_client.ts | 25 +++++++----------- src/webauthn_psk.ts | 7 ++++- 8 files changed, 117 insertions(+), 37 deletions(-) diff --git a/dist/chromium/options.html b/dist/chromium/options.html index f7984c7..258f002 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -24,6 +24,7 @@

PSK Options

+

diff --git a/src/background.ts b/src/background.ts index d5b6f5b..88ee62e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -6,6 +6,7 @@ import {getOriginFromUrl, webauthnParse, webauthnStringify} from './utils'; import {createPublicKeyCredential, getPublicKeyCredential} from "./webauthn_client"; import {PSK} from "./webauthn_psk"; +import {PinStorage} from "./webauth_storage"; const log = getLogger('background'); @@ -93,6 +94,18 @@ const pskOptions = async (alias, url) => { await PSK.setOptions(alias, url); }; +const authSetup = async () => { + let pin = await PinStorage.getPin().catch(_ => null); + if (pin != null) { + throw new Error("PIN already set"); + } + pin = prompt("Please enter a PIN for the authenticator", ""); + if (pin == null) { + throw new Error("Invalid PIN"); + } + return await PinStorage.setPin(pin); +} + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { switch (msg.type) { case 'create_credential': @@ -107,6 +120,9 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'psk_options': pskOptions(msg.alias, msg.url).then(() => alert('PSK options was successfully!'), e => log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e)); break; + case 'auth_setup': + authSetup().then(() => alert('Authenticator setup was successful.'), e => alert(e)); + break; case 'user_consent': const cb = userConsentCallbacks[msg.tabId]; if (!cb) { diff --git a/src/constants.ts b/src/constants.ts index 1c56acf..78df5f2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -20,8 +20,7 @@ export const ES256_COSE = -7; export const ES256 = 'P-256'; export const SHA256_COSE = 1; -export const PIN = '0000'; - +export const PIN = 'pin'; export const PSK_EXTENSION_IDENTIFIER = 'psk'; export const BACKUP_KEY = 'backup_key'; export const BD_ENDPOINT = 'bd_endpoint'; diff --git a/src/options.ts b/src/options.ts index 5a1e6ed..5fd73ad 100644 --- a/src/options.ts +++ b/src/options.ts @@ -18,4 +18,11 @@ $(() => { url: $('#BackupDeviceUrl').val(), }); }); + + $('#Setup').on('click', function(evt: Event) { + evt.preventDefault(); + chrome.runtime.sendMessage({ + type: 'auth_setup', + }); + }); }); diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 6aa209d..240ff3e 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -1,21 +1,59 @@ -import {base64ToByteArray, byteArrayToBase64, concatenate} from "./utils"; +import { + base64ToByteArray, + byteArrayToBase64, + concatenate, +} from "./utils"; import { BACKUP_KEY, BD, BD_ENDPOINT, DEFAULT_BD_ENDPOINT, ES256, ivLength, keyExportFormat, - PIN, RECOVERY_KEY, - saltLength + saltLength, + PIN } from "./constants"; import {getLogger} from "./logging"; import {BackupKey, RecoveryKey} from "./webauthn_psk"; const log = getLogger('auth_storage'); +export class PinStorage { + public static async getPin(): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.get({[PIN]: null}, async (resp) => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.getPin', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } + + if (resp[PIN] == null) { + rej('No PIN available. Have you performed the setup for your authenticator?'); + } + log.debug('Loaded PIN endpoint successfully'); + res(resp[PIN]); + }); + }); + }; + + public static async setPin(pin: string): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.set({[PIN]: pin}, () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.setPin', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } else { + res(); + } + }); + }); + } +} + export class PSKStorage { - public static async getBDEndpoint(): Promise { + public static async getBDEndpoint(): Promise { return new Promise(async (res, rej) => { chrome.storage.local.get({[BD_ENDPOINT]: null}, async (resp) => { if (!!chrome.runtime.lastError) { @@ -369,7 +407,7 @@ export class PublicKeyCredentialSource { async function exportKey(key: CryptoKey): Promise { const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(PIN, salt); + const wrappingKey = await getWrappingKey(await PinStorage.getPin(), salt); const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); const wrapAlgorithm: AesGcmParams = { iv, @@ -408,7 +446,7 @@ async function importKey(rawKey: string): Promise { offset += keyAlgorithmByteLength; const keyBytes = keyPayload.subarray(offset); - const wrappingKey = await getWrappingKey(PIN, salt); + const wrappingKey = await getWrappingKey(await PinStorage.getPin(), salt); const wrapAlgorithm: AesGcmParams = { iv, name: 'AES-GCM', diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index e26dff4..63cb3e1 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -1,5 +1,5 @@ import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; -import {CredentialsMap, PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; +import {CredentialsMap, PinStorage, PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; import {base64ToByteArray, byteArrayToBase64, counterToBytes} from "./utils"; import * as CBOR from 'cbor'; import {createAttestationSignature, getAttestationCertificate} from "./webauthn_attestation"; @@ -92,6 +92,14 @@ export class Authenticator { throw new Error(`no user consent`); } + let uv = false; + if (requireUserVerification) { + uv = await this.verifyUser("The relying party requires user verification."); + if (!uv) { + throw new Error(`user verification failed`); + } + } + // Step 8 let processedExtensions = undefined; if (extensions) { @@ -125,7 +133,7 @@ export class Authenticator { // Step 10 const authenticatorData = await this.generateAuthenticatorData(rpId, - this.getSignatureCounter(), undefined, processedExtensions); + this.getSignatureCounter(), undefined, processedExtensions, uv); // Step 11 const concatData = new Uint8Array(authenticatorData.length + hash.length); @@ -178,22 +186,24 @@ export class Authenticator { // Step 4 Not needed, because cKey supports resident keys - // Step 5 - if (requireUserVerification) { - throw new Error(`authenticator does not support user verification`); - } - - // Step 6 + // Step 5 + 6 const userConsent = await userConsentCallback; if (!userConsent) { throw new Error(`no user consent`); } - userEntity.id - return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, undefined, extensions, userEntity.id); + let uv = false; + if (requireUserVerification) { + uv = await this.verifyUser("The relying party requires user verification."); + if (!uv) { + throw new Error(`user verification failed`); + } + } + + return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, undefined, extensions, userEntity.id); } - public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { + public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, uv: boolean, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { // Step 7 if (!(keyPair)) { log.debug('No key pair provided, create new one.'); @@ -234,7 +244,7 @@ export class Authenticator { const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); // Step 12 - const authenticatorData = await this.generateAuthenticatorData(rpId, sigCnt, attestedCredentialData, processedExtensions); + const authenticatorData = await this.generateAuthenticatorData(rpId, sigCnt, attestedCredentialData, processedExtensions, uv); // Step 13 const attObj = await this.generateAttestationObject(hash, authenticatorData); @@ -272,7 +282,7 @@ export class Authenticator { } private static async generateAuthenticatorData(rpID: string, counter: number, attestedCredentialData?: Uint8Array, - extensionData?: Uint8Array): Promise { + extensionData?: Uint8Array, uv?: boolean): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); let authenticatorDataLength = rpIdHash.length + 1 + 4; @@ -292,6 +302,9 @@ export class Authenticator { // 1 byte for flags authenticatorData[rpIdHash.length] = 1; // UP + if (uv) { + authenticatorData[rpIdHash.length] |= (1 << 2); // AT + } if (attestedCredentialData) { authenticatorData[rpIdHash.length] |= (1 << 6); // AT } @@ -341,4 +354,10 @@ export class Authenticator { }); return byteArrayToBase64(enc.encode(uuid), true); } + + public static async verifyUser(message: string): Promise { + const userPin = prompt(`${message}\nPlease enter your PIN.`, ""); + const originPin = await PinStorage.getPin(); + return userPin == originPin + } } \ No newline at end of file diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 8b80c04..4f46908 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -53,26 +53,22 @@ export async function createPublicKeyCredential(origin: string, options: Credent // Handle only 1 authenticator // Step 20, simplified + let userVerification = true; + let residentKey = false; if (options.publicKey.authenticatorSelection) { if (options.publicKey.authenticatorSelection.authenticatorAttachment && (options.publicKey.authenticatorSelection.authenticatorAttachment !== 'platform')) { throw new Error(`${options.publicKey.authenticatorSelection.authenticatorAttachment} authenticator requested, but only platform authenticators available`); } + if (options.publicKey.authenticatorSelection.requireResidentKey) { + residentKey = options.publicKey.authenticatorSelection.requireResidentKey; + } - // Resident key check can be omitted, because cKey supports resident keys - - if (options.publicKey.authenticatorSelection.userVerification && (options.publicKey.authenticatorSelection.userVerification === 'required')) { - throw new Error(`cKey does not support user verification`); + if (options.publicKey.authenticatorSelection.userVerification && (options.publicKey.authenticatorSelection.userVerification === 'discouraged')) { + userVerification = false; } } - - let userVerification = false; - let residentKey = false; - if (options.publicKey.authenticatorSelection) { - userVerification = options.publicKey.authenticatorSelection.requireUserVerification === "required"; - residentKey = options.publicKey.authenticatorSelection.requireResidentKey; - } const userPresence = !userVerification; const excludeCredentialDescriptorList = options.publicKey.excludeCredentials // No filtering @@ -151,11 +147,10 @@ export async function getPublicKeyCredential(origin: string, options: Credential // Handle only 1 authenticator // Step 18 - if (options.publicKey.userVerification && (options.publicKey.userVerification === 'required')) { - throw new Error(`cKey does not support user verification`); + let userVerification = true; + if (options.publicKey.userVerification && (options.publicKey.userVerification === 'discouraged')) { + userVerification = false; } - - const userVerification = options.publicKey.userVerification === "required"; const userPresence = !userVerification; const allowCredentialDescriptorList = options.publicKey.allowCredentials; // No filtering diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 28455da..dd113b9 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -83,6 +83,11 @@ export class PSK { public static async sync(): Promise { log.debug('Sync triggered'); + const verified = await Authenticator.verifyUser("User verification for PSK sync required."); + if (!verified) { + throw new Error(`user verification failed for PSK sync`); + } + const bdEndpoint = await PSKStorage.getBDEndpoint(); return await axios.default.get(bdEndpoint + '/sync', {timeout: BD_TIMEOUT}) @@ -189,7 +194,7 @@ export class PSK { keyPair.publicKey = recKey.pubKey; const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, keyPair, authenticatorExtensions); + const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, false, keyPair, authenticatorExtensions); log.debug('Delegation signature', recKey.delegationSignature); log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); From acc2c7d23784db0188d30dae6497ec168bc3de38 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 9 Oct 2020 19:38:23 +0200 Subject: [PATCH 66/81] Add user verification and encryption --- dist/chromium/options.html | 2 +- package-lock.json | 5 +++++ package.json | 1 + src/background.ts | 10 ++++++---- src/inject_webauthn.ts | 2 +- src/webauth_storage.ts | 23 +++++++++++++++++------ src/webauthn_authenticator.ts | 29 +++++++++++++++-------------- src/webauthn_client.ts | 2 +- 8 files changed, 47 insertions(+), 27 deletions(-) diff --git a/dist/chromium/options.html b/dist/chromium/options.html index 258f002..408ae1a 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -24,7 +24,7 @@

PSK Options

- +

diff --git a/package-lock.json b/package-lock.json index 0600921..ce6174a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2223,6 +2223,11 @@ "tweetnacl": "^0.14.3" } }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", diff --git a/package.json b/package.json index 5f6e13c..5cbfa43 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@types/webappsec-credential-management": "^0.3.11", "asn1js": "^2.0.26", "axios": "^0.19.2", + "bcryptjs": "^2.4.3", "bn.js": "^5.1.2", "cbor": "^4.3.0", "ec-key": "0.0.4", diff --git a/src/background.ts b/src/background.ts index 88ee62e..a52d1ba 100644 --- a/src/background.ts +++ b/src/background.ts @@ -25,11 +25,12 @@ const requestUserConsent = async (tabId: number, origin: string): Promise = new Promise((res, _) => { + chrome.pageAction.setIcon({ tabId, path: enabledIcons }); + chrome.pageAction.setPopup({ tabId, popup: 'popup.html' }); + chrome.pageAction.show(tabId); userConsentCallbacks[tabId] = res; }); - chrome.pageAction.setIcon({ tabId, path: enabledIcons }); - chrome.pageAction.setPopup({ tabId, popup: 'popup.html' }); - chrome.pageAction.show(tabId); + const userConsent = await cb; chrome.storage.local.remove(tabKey); chrome.pageAction.setPopup({ tabId, popup: '' }); @@ -79,6 +80,7 @@ const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { return { credential: webauthnStringify(credential), requestID: msg.requestID, + clientExtensionResults: credential.getClientExtensionResults(), type: 'get_credential_response', }; } catch (e) { @@ -95,7 +97,7 @@ const pskOptions = async (alias, url) => { }; const authSetup = async () => { - let pin = await PinStorage.getPin().catch(_ => null); + let pin = await PinStorage.getPinHash().catch(_ => null); if (pin != null) { throw new Error("PIN already set"); } diff --git a/src/inject_webauthn.ts b/src/inject_webauthn.ts index 357081c..ffc40e7 100644 --- a/src/inject_webauthn.ts +++ b/src/inject_webauthn.ts @@ -44,7 +44,7 @@ const log = getLogger('inject_webauthn'); window.postMessage(getCredentialRequest, window.location.origin); const webauthnResponse = await cb; const credential = webauthnParse(webauthnResponse.resp.credential); - credential.getClientExtensionResults = () => ({}); // ToDo Return actual client extension result + credential.getClientExtensionResults = () => (webauthnResponse.resp.clientExtensionResults); credential.__proto__ = window['PublicKeyCredential'].prototype; return credential; }; diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index 240ff3e..a44a126 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -18,9 +18,16 @@ import {BackupKey, RecoveryKey} from "./webauthn_psk"; const log = getLogger('auth_storage'); +export let SESSION_PIN = null + export class PinStorage { - public static async getPin(): Promise { - return new Promise(async (res, rej) => { + private static saltRounds = 10; + public static setSessionPIN(pin: string) { + SESSION_PIN = pin; + } + + public static async getPinHash(): Promise { + return new Promise(async (res, rej) => { chrome.storage.local.get({[PIN]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.getPin', chrome.runtime.lastError.message); @@ -31,20 +38,24 @@ export class PinStorage { if (resp[PIN] == null) { rej('No PIN available. Have you performed the setup for your authenticator?'); } - log.debug('Loaded PIN endpoint successfully'); + log.debug('Loaded PIN hash successfully'); res(resp[PIN]); }); }); }; public static async setPin(pin: string): Promise { + const bcrypt = require('bcryptjs'); + let hash = bcrypt.hashSync(pin, this.saltRounds); + return new Promise(async (res, rej) => { - chrome.storage.local.set({[PIN]: pin}, () => { + chrome.storage.local.set({[PIN]: hash}, () => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.setPin', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); return; } else { + log.debug('Set PIN successfully'); res(); } }); @@ -407,7 +418,7 @@ export class PublicKeyCredentialSource { async function exportKey(key: CryptoKey): Promise { const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(await PinStorage.getPin(), salt); + const wrappingKey = await getWrappingKey(SESSION_PIN, salt); const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); const wrapAlgorithm: AesGcmParams = { iv, @@ -446,7 +457,7 @@ async function importKey(rawKey: string): Promise { offset += keyAlgorithmByteLength; const keyBytes = keyPayload.subarray(offset); - const wrappingKey = await getWrappingKey(await PinStorage.getPin(), salt); + const wrappingKey = await getWrappingKey(SESSION_PIN, salt); const wrapAlgorithm: AesGcmParams = { iv, name: 'AES-GCM', diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 63cb3e1..e89a034 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -92,12 +92,10 @@ export class Authenticator { throw new Error(`no user consent`); } - let uv = false; - if (requireUserVerification) { - uv = await this.verifyUser("The relying party requires user verification."); - if (!uv) { - throw new Error(`user verification failed`); - } + // USer verification is always performed, because PIN is needed to decrypt keys + let uv = await this.verifyUser("User verification is required."); + if (!uv) { + throw new Error(`user verification failed`); } // Step 8 @@ -192,12 +190,10 @@ export class Authenticator { throw new Error(`no user consent`); } - let uv = false; - if (requireUserVerification) { - uv = await this.verifyUser("The relying party requires user verification."); - if (!uv) { - throw new Error(`user verification failed`); - } + // USer verification is always performed, because PIN is needed to decrypt keys + let uv = await this.verifyUser("The relying party requires user verification."); + if (!uv) { + throw new Error(`user verification failed`); } return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, undefined, extensions, userEntity.id); @@ -356,8 +352,13 @@ export class Authenticator { } public static async verifyUser(message: string): Promise { + const bcrypt = require('bcryptjs'); + const userPin = prompt(`${message}\nPlease enter your PIN.`, ""); - const originPin = await PinStorage.getPin(); - return userPin == originPin + const pinHash = await PinStorage.getPinHash(); + const match = bcrypt.compareSync(userPin, pinHash); + + PinStorage.setSessionPIN(userPin); + return match } } \ No newline at end of file diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 4f46908..0081a6c 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -91,7 +91,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent } return { - getClientExtensionResults: () => (clientExtensions), // ToDo Fix client extension output + getClientExtensionResults: () => (clientExtensions), id: credentialId, rawId: base64ToByteArray(credentialId, true), response: { From 6093413b9bbfd34e212c74733d558c8f63833ba4 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sat, 10 Oct 2020 19:53:38 +0200 Subject: [PATCH 67/81] Remove base64 in PSK extension --- src/webauthn_psk.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index dd113b9..7c14d32 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -198,11 +198,12 @@ export class PSK { log.debug('Delegation signature', recKey.delegationSignature); log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); + log.debug('BDData', recKey.bdData); // Finally remove recovery key since PSK output was generated successfully await RecoveryKey.removeRecoveryKey(oldBackupKeyId); - const recoveryMessage = {attestationObject: byteArrayToBase64(rawAttObj, true), oldBackupKeyId: oldBackupKeyId, delegationSignature: recKey.delegationSignature, bdData: recKey.bdData} + const recoveryMessage = {attestationObject: rawAttObj, oldBackupKeyId: base64ToByteArray(oldBackupKeyId, true), delegationSignature: base64ToByteArray(recKey.delegationSignature, true), bdData: base64ToByteArray(recKey.bdData, true)} return [credentialId, recoveryMessage] } } \ No newline at end of file From fb368d5666107c325b475779f52c0000651f385b Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 12 Oct 2020 21:39:25 +0200 Subject: [PATCH 68/81] Fix icon display bug --- src/background.ts | 11 ++++++----- src/constants.ts | 2 -- src/webauthn_authenticator.ts | 28 +++++++++++++++------------- src/webauthn_client.ts | 6 +++--- src/webauthn_psk.ts | 2 +- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/background.ts b/src/background.ts index a52d1ba..bbc041b 100644 --- a/src/background.ts +++ b/src/background.ts @@ -24,13 +24,14 @@ const requestUserConsent = async (tabId: number, origin: string): Promise = new Promise((res, _) => { - chrome.pageAction.setIcon({ tabId, path: enabledIcons }); - chrome.pageAction.setPopup({ tabId, popup: 'popup.html' }); - chrome.pageAction.show(tabId); userConsentCallbacks[tabId] = res; }); + chrome.pageAction.setIcon({ tabId, path: enabledIcons }); + chrome.pageAction.setPopup({ tabId, popup: 'popup.html' }); + chrome.pageAction.show(tabId); const userConsent = await cb; chrome.storage.local.remove(tabKey); chrome.pageAction.setPopup({ tabId, popup: '' }); @@ -46,7 +47,7 @@ const createCredential = async (msg, sender: chrome.runtime.MessageSender) => { } const opts = webauthnParse(msg.options); const origin = getOriginFromUrl(sender.url); - const userConsentCB = requestUserConsent(sender.tab.id, origin); + const userConsentCB = function() { return requestUserConsent(sender.tab.id, origin); } try { const credential = await createPublicKeyCredential( @@ -73,7 +74,7 @@ const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { } const opts = webauthnParse(msg.options); const origin = getOriginFromUrl(sender.url); - const userConsentCB = requestUserConsent(sender.tab.id, origin); + const userConsentCB = function() { return requestUserConsent(sender.tab.id, origin); } try { const credential = await getPublicKeyCredential(origin, opts, true, userConsentCB); diff --git a/src/constants.ts b/src/constants.ts index 78df5f2..bd1e695 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,7 +26,5 @@ export const BACKUP_KEY = 'backup_key'; export const BD_ENDPOINT = 'bd_endpoint'; export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005'; export const RECOVERY_KEY = 'recovery_key'; -export const AUTH_ALIAS = 'auth_alias'; -export const DEFAULT_AUTH_ALIAS = 'MyAuth'; export const BD_TIMEOUT = 60 * 1000 * 10; // 10 minutes export const BD = 'bd' diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index e89a034..e9fb91a 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -35,7 +35,7 @@ export class Authenticator { return 0; } - public static async authenticatorGetAssertion(userConsentCallback: Promise, + public static async authenticatorGetAssertion(userConsentCallback: () => Promise, rpId: string, hash: Uint8Array, requireUserPresence: boolean, @@ -87,8 +87,8 @@ export class Authenticator { credSource = credentialOptions[0]; } - const userConsent = await userConsentCallback; - if (!userConsent) { + const up = await userConsentCallback(); + if (!up) { throw new Error(`no user consent`); } @@ -131,7 +131,7 @@ export class Authenticator { // Step 10 const authenticatorData = await this.generateAuthenticatorData(rpId, - this.getSignatureCounter(), undefined, processedExtensions, uv); + this.getSignatureCounter(), undefined, processedExtensions, up, uv); // Step 11 const concatData = new Uint8Array(authenticatorData.length + hash.length); @@ -144,7 +144,7 @@ export class Authenticator { return new AssertionResponse(credSource.id, authenticatorData, signature, credSource.userHandle); } - public static async authenticatorMakeCredential(userConsentCallback: Promise, + public static async authenticatorMakeCredential(userConsentCallback: () => Promise, hash: Uint8Array, rpEntity: PublicKeyCredentialRpEntity, userEntity: PublicKeyCredentialUserEntity, @@ -185,21 +185,21 @@ export class Authenticator { // Step 4 Not needed, because cKey supports resident keys // Step 5 + 6 - const userConsent = await userConsentCallback; - if (!userConsent) { + const up = await userConsentCallback(); // User presence always checked + if (!up) { throw new Error(`no user consent`); } - // USer verification is always performed, because PIN is needed to decrypt keys + // User verification is always performed, because PIN is needed to decrypt keys let uv = await this.verifyUser("The relying party requires user verification."); if (!uv) { throw new Error(`user verification failed`); } - return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, undefined, extensions, userEntity.id); + return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, up,undefined, extensions, userEntity.id); } - public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, uv: boolean, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { + public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, uv: boolean, up:boolean, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { // Step 7 if (!(keyPair)) { log.debug('No key pair provided, create new one.'); @@ -240,7 +240,7 @@ export class Authenticator { const attestedCredentialData = await this.generateAttestedCredentialData(rawCredentialId, keyPair); // Step 12 - const authenticatorData = await this.generateAuthenticatorData(rpId, sigCnt, attestedCredentialData, processedExtensions, uv); + const authenticatorData = await this.generateAuthenticatorData(rpId, sigCnt, attestedCredentialData, processedExtensions, up, uv); // Step 13 const attObj = await this.generateAttestationObject(hash, authenticatorData); @@ -278,7 +278,7 @@ export class Authenticator { } private static async generateAuthenticatorData(rpID: string, counter: number, attestedCredentialData?: Uint8Array, - extensionData?: Uint8Array, uv?: boolean): Promise { + extensionData?: Uint8Array, up?: boolean, uv?: boolean): Promise { const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID)); const rpIdHash = new Uint8Array(rpIdDigest); let authenticatorDataLength = rpIdHash.length + 1 + 4; @@ -297,7 +297,9 @@ export class Authenticator { offset += rpIdHash.length; // 1 byte for flags - authenticatorData[rpIdHash.length] = 1; // UP + if (up) { + authenticatorData[rpIdHash.length] = 1; // UP + } if (uv) { authenticatorData[rpIdHash.length] |= (1 << 2); // AT } diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 0081a6c..47256e2 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -10,7 +10,7 @@ const Get: FunctionType = "webauthn.get"; const log = getLogger('webauthn_client'); -export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise): Promise { +export async function createPublicKeyCredential(origin: string, options: CredentialCreationOptions, sameOriginWithAncestors: boolean, userConsentCallback: () => Promise): Promise { log.debug('Called createPublicKeyCredential'); // Step 1 @@ -102,7 +102,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent } as PublicKeyCredential; } -export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean, userConsentCallback: Promise) { +export async function getPublicKeyCredential(origin: string, options: CredentialRequestOptions, sameOriginWithAncestors: boolean, userConsentCallback: () => Promise) { // Step 1 if (!options.publicKey) { throw new Error('options missing'); @@ -166,7 +166,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential log.debug('Received assertion response'); return { - getClientExtensionResults: () => (clientExtensions), // ToDo Add client extension output + getClientExtensionResults: () => (clientExtensions), id: assertionCreationData.credentialId, rawId: base64ToByteArray(assertionCreationData.credentialId, true), response: { diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 7c14d32..646aade 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -194,7 +194,7 @@ export class PSK { keyPair.publicKey = recKey.pubKey; const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, false, keyPair, authenticatorExtensions); + const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, true, true, keyPair, authenticatorExtensions); log.debug('Delegation signature', recKey.delegationSignature); log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); From 427d32afd4ebb9611b78dca435bdbadf6d7dfa5b Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 21 Oct 2020 16:09:35 +0200 Subject: [PATCH 69/81] Update names --- README.md | 7 ++++++- dist/chromium/options.html | 2 +- src/background.ts | 10 +++++----- src/options.ts | 6 +++--- src/webauthn_authenticator.ts | 24 ++++++++++++------------ src/webauthn_psk.ts | 23 +++++++++-------------- 6 files changed, 36 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 286402d..86c4617 100644 --- a/README.md +++ b/README.md @@ -22,4 +22,9 @@ $ npm run watch # Loading into the browser -You can load the project as an unpacked extension. Upon building, you may load the directory `dist/chromium/` into your browser. More details on how to do this [here](https://developer.chrome.com/extensions/getstarted). \ No newline at end of file +You can load the project as an unpacked extension. Upon building, you may load the directory `dist/chromium/` into your browser. More details on how to do this [here](https://developer.chrome.com/extensions/getstarted). + +# PSK Protocol Support + +- Support for PSK WebAuthn Extension +- Support for PSK Setup API \ No newline at end of file diff --git a/dist/chromium/options.html b/dist/chromium/options.html index 408ae1a..25c9c95 100644 --- a/dist/chromium/options.html +++ b/dist/chromium/options.html @@ -24,7 +24,7 @@

PSK Options

- +

diff --git a/src/background.ts b/src/background.ts index bbc041b..f6ce87a 100644 --- a/src/background.ts +++ b/src/background.ts @@ -117,14 +117,14 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { case 'get_credential': getCredential(msg, sender).then(sendResponse); break; - case 'psk_sync': - pskSync().then(() => alert('PSK sync was successfully!'), e => log.error('failed to sync psk', { errorType: `${(typeof e)}` }, e)); + case 'psk_setup': + pskSync().then(() => alert('PSK setup flow was successful.'), e => log.error('PSK setup flow failed', { errorType: `${(typeof e)}` }, e)); break; case 'psk_options': - pskOptions(msg.alias, msg.url).then(() => alert('PSK options was successfully!'), e => log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e)); + pskOptions(msg.alias, msg.url).then(() => alert('PSK options set successfully.'), e => log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e)); break; - case 'auth_setup': - authSetup().then(() => alert('Authenticator setup was successful.'), e => alert(e)); + case 'auth_pin_set': + authSetup().then(() => alert('Authenticator PIN setup was successful.'), e => alert(e)); break; case 'user_consent': const cb = userConsentCallbacks[msg.tabId]; diff --git a/src/options.ts b/src/options.ts index 5fd73ad..cd791e3 100644 --- a/src/options.ts +++ b/src/options.ts @@ -7,7 +7,7 @@ $(() => { $('#Sync').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'psk_sync', + type: 'psk_setup', }); }); @@ -19,10 +19,10 @@ $(() => { }); }); - $('#Setup').on('click', function(evt: Event) { + $('#Pin').on('click', function(evt: Event) { evt.preventDefault(); chrome.runtime.sendMessage({ - type: 'auth_setup', + type: 'auth_pin_set', }); }); }); diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index e9fb91a..fedb250 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -1,11 +1,11 @@ import {ECDSA, ICOSECompatibleKey} from "./webauthn_crypto"; -import {CredentialsMap, PinStorage, PSKStorage, PublicKeyCredentialSource} from "./webauth_storage"; +import {CredentialsMap, PinStorage, PublicKeyCredentialSource} from "./webauth_storage"; import {base64ToByteArray, byteArrayToBase64, counterToBytes} from "./utils"; import * as CBOR from 'cbor'; import {createAttestationSignature, getAttestationCertificate} from "./webauthn_attestation"; import {getLogger} from "./logging"; import {ES256_COSE, PSK_EXTENSION_IDENTIFIER} from "./constants"; -import {PSK} from "./webauthn_psk"; +import {PSK, RecoveryKey} from "./webauthn_psk"; const log = getLogger('webauthn_authenticator'); @@ -47,7 +47,7 @@ export class Authenticator { log.debug('Called authenticatorGetAssertion'); // Step 2-7 + recovery lookup - let isRecovery: [boolean, string] = [false, ""]; + let isRecovery: RecoveryKey = null; let credentialOptions: PublicKeyCredentialSource[] = []; if (allowCredentialDescriptorList) { // Simplified credential lookup @@ -69,21 +69,20 @@ export class Authenticator { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - const recExists = await PSKStorage.recoveryKeyExists(credId); - if (recExists) { + isRecovery = await RecoveryKey.findRecoveryKey(credId); + if (isRecovery != null) { log.info('Recovery detected for', credId); - isRecovery = [true, credId]; break; } } - if (!isRecovery[0]) { + if (isRecovery == null) { // No recovery and no associated credential found throw new Error(`Container does not manage any related credentials`); } } // Note: The authenticator won't let the user select a public key credential source let credSource; - if (!isRecovery[0]) { // No recovery + if (isRecovery == null) { // No recovery credSource = credentialOptions[0]; } @@ -103,12 +102,12 @@ export class Authenticator { if (extensions) { if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('Get: PSK requested'); - if (!isRecovery[0]) { + if (isRecovery == null) { throw new Error('PSK extension requested, but no matching recovery key available'); } const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); const pskInput = await CBOR.decode(new Buffer(rawPskInput)); - const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery[1], pskInput, rpId); + const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery, pskInput, rpId); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); credSource = await CredentialsMap.lookup(rpId, newCredId); if (credSource == null) { @@ -116,10 +115,10 @@ export class Authenticator { throw new Error('Get: New credential source missing'); } log.debug('Get: Processed PSK'); - } else if (isRecovery[0]) { + } else if (isRecovery != null) { throw new Error('Recovery detected, but no PSK requested.') } - } else if (isRecovery[0]) { + } else if (isRecovery != null) { throw new Error('Recovery detected, but no PSK requested.') } @@ -229,6 +228,7 @@ export class Authenticator { } if (processedExtensions) { processedExtensions = new Uint8Array(CBOR.encodeCanonical(processedExtensions)); + log.debug('CBOR extension', Buffer.from(processedExtensions).toString('hex')); } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 646aade..9d845ac 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -83,9 +83,9 @@ export class PSK { public static async sync(): Promise { log.debug('Sync triggered'); - const verified = await Authenticator.verifyUser("User verification for PSK sync required."); + const verified = await Authenticator.verifyUser("User verification for PSK setup flow required."); if (!verified) { - throw new Error(`user verification failed for PSK sync`); + throw new Error(`user verification failed for PSK setup flow`); } const bdEndpoint = await PSKStorage.getBDEndpoint(); @@ -181,29 +181,24 @@ export class PSK { return raw_backup_keys; } - public static async authenticatorGetCredentialExtensionOutput(oldBackupKeyId: string, customClientDataHash: Uint8Array, rpId: string): Promise<[string, any]> { + public static async authenticatorGetCredentialExtensionOutput(recoveryKey: RecoveryKey, customClientDataHash: Uint8Array, rpId: string): Promise<[string, any]> { log.debug('authenticatorGetCredentialExtensionOutput called'); - // Find recovery key for given credential id - const recKey = await RecoveryKey.findRecoveryKey(oldBackupKeyId); - if (recKey == null) { - throw new Error("No recovery key found, but recovery was detected"); - } // Create attestation object using the key pair of the recovery key + request PSK extension - const keyPair = await ECDSA.fromKey(recKey.privKey); - keyPair.publicKey = recKey.pubKey; + const keyPair = await ECDSA.fromKey(recoveryKey.privKey); + keyPair.publicKey = recoveryKey.pubKey; const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, true, true, keyPair, authenticatorExtensions); - log.debug('Delegation signature', recKey.delegationSignature); + log.debug('Delegation signature', recoveryKey.delegationSignature); log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); - log.debug('BDData', recKey.bdData); + log.debug('BDData', recoveryKey.bdData); // Finally remove recovery key since PSK output was generated successfully - await RecoveryKey.removeRecoveryKey(oldBackupKeyId); + await RecoveryKey.removeRecoveryKey(recoveryKey.backupKeyId); - const recoveryMessage = {attestationObject: rawAttObj, oldBackupKeyId: base64ToByteArray(oldBackupKeyId, true), delegationSignature: base64ToByteArray(recKey.delegationSignature, true), bdData: base64ToByteArray(recKey.bdData, true)} + const recoveryMessage = {attestationObject: rawAttObj, oldBackupKeyId: base64ToByteArray(recoveryKey.backupKeyId, true), delegationSignature: base64ToByteArray(recoveryKey.delegationSignature, true), bdData: base64ToByteArray(recoveryKey.bdData, true)} return [credentialId, recoveryMessage] } } \ No newline at end of file From d5ac8c38a32a3f58bd78962bcf261a4a148d9099 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 23 Oct 2020 14:47:10 +0200 Subject: [PATCH 70/81] Add user handle to PSK extension --- src/background.ts | 2 +- src/webauthn_authenticator.ts | 36 +++++++++++++++++++++++++---------- src/webauthn_client.ts | 17 ++++++----------- src/webauthn_psk.ts | 14 +++++++------- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/src/background.ts b/src/background.ts index f6ce87a..23d2f0f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -90,7 +90,7 @@ const getCredential = async (msg, sender: chrome.runtime.MessageSender) => { }; const pskSync = async () => { - await PSK.sync(); + await PSK.pskSetup(); }; const pskOptions = async (alias, url) => { diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index fedb250..0b1b59c 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -47,7 +47,7 @@ export class Authenticator { log.debug('Called authenticatorGetAssertion'); // Step 2-7 + recovery lookup - let isRecovery: RecoveryKey = null; + let recKey: RecoveryKey = null; let credentialOptions: PublicKeyCredentialSource[] = []; if (allowCredentialDescriptorList) { // Simplified credential lookup @@ -69,20 +69,20 @@ export class Authenticator { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - isRecovery = await RecoveryKey.findRecoveryKey(credId); - if (isRecovery != null) { + recKey = await RecoveryKey.findRecoveryKey(credId); + if (recKey != null) { log.info('Recovery detected for', credId); break; } } - if (isRecovery == null) { + if (recKey == null) { // No recovery and no associated credential found throw new Error(`Container does not manage any related credentials`); } } // Note: The authenticator won't let the user select a public key credential source let credSource; - if (isRecovery == null) { // No recovery + if (recKey == null) { // No recovery credSource = credentialOptions[0]; } @@ -102,12 +102,26 @@ export class Authenticator { if (extensions) { if (extensions.has(PSK_EXTENSION_IDENTIFIER)) { log.debug('Get: PSK requested'); - if (isRecovery == null) { + if (recKey == null) { throw new Error('PSK extension requested, but no matching recovery key available'); } const rawPskInput = base64ToByteArray(extensions.get(PSK_EXTENSION_IDENTIFIER), true); const pskInput = await CBOR.decode(new Buffer(rawPskInput)); - const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(isRecovery, pskInput, rpId); + if (!pskInput.hasOwnProperty('customClientDataHash')) { + throw new Error("PSK extension input has no customClientDataHash"); + } + const customClientDataHash = pskInput['customClientDataHash']; + if (!pskInput.hasOwnProperty('userHandle')) { + throw new Error("PSK extension input has no userHandle"); + } + const userHandle = new Uint8Array(pskInput['userHandle']).buffer; + if (Buffer.byteLength(pskInput.customClientDataHash) != 32) { + throw new Error("PSK extension: customClientDataHash has invalid length") + } + if (userHandle.byteLength == 0) { + throw new Error("PSK extension: user handle has invalid length") + } + const [newCredId, pskOutput] = await PSK.authenticatorGetCredentialExtensionOutput(recKey, customClientDataHash, userHandle, rpId); processedExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, pskOutput]]); credSource = await CredentialsMap.lookup(rpId, newCredId); if (credSource == null) { @@ -115,10 +129,10 @@ export class Authenticator { throw new Error('Get: New credential source missing'); } log.debug('Get: Processed PSK'); - } else if (isRecovery != null) { + } else if (recKey != null) { throw new Error('Recovery detected, but no PSK requested.') } - } else if (isRecovery != null) { + } else if (recKey != null) { throw new Error('Recovery detected, but no PSK requested.') } @@ -360,7 +374,9 @@ export class Authenticator { const pinHash = await PinStorage.getPinHash(); const match = bcrypt.compareSync(userPin, pinHash); - PinStorage.setSessionPIN(userPin); + if (match) { + PinStorage.setSessionPIN(userPin); + } return match } } \ No newline at end of file diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 47256e2..a290277 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -124,17 +124,12 @@ export async function getPublicKeyCredential(origin: string, options: Credential if (options.publicKey.extensions) { const reqExt: any = options.publicKey.extensions; if (reqExt.hasOwnProperty(PSK_EXTENSION_IDENTIFIER)) { - log.debug('PSK extension requested'); - if (reqExt[PSK_EXTENSION_IDENTIFIER] == true) { - log.debug('PSK extension has valid client input'); - const customClientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); - const customClientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(customClientDataJSON))); - const customClientDataHash = new Uint8Array(customClientDataHashDigest); - const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(customClientDataHash)); - authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - } else { - log.warn('PSK client extension processing failed. Wrong input.'); - } + const userHandle = base64ToByteArray(reqExt[PSK_EXTENSION_IDENTIFIER], true); + const customClientDataJSON = generateClientDataJSON(Create, options.publicKey.challenge as ArrayBuffer, origin); + const customClientDataHashDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(JSON.stringify(customClientDataJSON))); + const customClientDataHash = new Uint8Array(customClientDataHashDigest); + const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({customClientDataHash: customClientDataHash, userHandle: userHandle})); + authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); } } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 9d845ac..edbf40b 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -80,8 +80,8 @@ export class PSK { return await PSKStorage.setBDEndpoint(url); } - public static async sync(): Promise { - log.debug('Sync triggered'); + public static async pskSetup(): Promise { + log.debug('pskSetup triggered'); const verified = await Authenticator.verifyUser("User verification for PSK setup flow required."); if (!verified) { @@ -105,13 +105,13 @@ export class PSK { await PSKStorage.storeBackupKeys(backupKeys, syncResponse.bdUUID); if (syncResponse.hasOwnProperty("recoveryOption")) { - await PSK.recoverySync(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); + await PSK.pskRecoverySetup(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); } }); } - private static async recoverySync(delegatedAuthAlias: string, originAuthAlias: string, keyAmount: number): Promise { - log.debug("Recovery setup triggered"); + private static async pskRecoverySetup(delegatedAuthAlias: string, originAuthAlias: string, keyAmount: number): Promise { + log.debug("pskRecoverySetup triggered"); const bdEndpoint = await PSKStorage.getBDEndpoint(); @@ -181,7 +181,7 @@ export class PSK { return raw_backup_keys; } - public static async authenticatorGetCredentialExtensionOutput(recoveryKey: RecoveryKey, customClientDataHash: Uint8Array, rpId: string): Promise<[string, any]> { + public static async authenticatorGetCredentialExtensionOutput(recoveryKey: RecoveryKey, customClientDataHash: Uint8Array, userHandle: ArrayBuffer, rpId: string): Promise<[string, any]> { log.debug('authenticatorGetCredentialExtensionOutput called'); // Create attestation object using the key pair of the recovery key + request PSK extension @@ -189,7 +189,7 @@ export class PSK { keyPair.publicKey = recoveryKey.pubKey; const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); const authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); - const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, true, true, keyPair, authenticatorExtensions); + const [credentialId, rawAttObj] = await Authenticator.finishAuthenticatorMakeCredential(rpId, customClientDataHash, true, true, keyPair, authenticatorExtensions, userHandle); log.debug('Delegation signature', recoveryKey.delegationSignature); log.debug('Attestation object', byteArrayToBase64(rawAttObj, true)); From 977deec15585c04cc7d7f1a9230ca1bdc6f4fe4d Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 23 Oct 2020 20:21:01 +0200 Subject: [PATCH 71/81] Add client extension output --- src/webauthn_client.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index a290277..7c39468 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -41,6 +41,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent log.info('PSK extension requested'); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical(true)); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; } } @@ -130,6 +131,7 @@ export async function getPublicKeyCredential(origin: string, options: Credential const customClientDataHash = new Uint8Array(customClientDataHashDigest); const authenticatorExtensionInput = new Uint8Array(CBOR.encodeCanonical({customClientDataHash: customClientDataHash, userHandle: userHandle})); authenticatorExtensions = new Map([[PSK_EXTENSION_IDENTIFIER, byteArrayToBase64(authenticatorExtensionInput, true)]]); + clientExtensions = {[PSK_EXTENSION_IDENTIFIER]: true}; } } From c4f0194d1d06371725138059b49ad0d02d1b8172 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Mon, 26 Oct 2020 09:33:26 +0100 Subject: [PATCH 72/81] Fox encrypted storage operations before UV is finished --- src/webauth_storage.ts | 39 ++++++++++++++++++++++------------ src/webauthn_authenticator.ts | 40 +++++++++++++++++++++++------------ src/webauthn_psk.ts | 7 +++--- 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index a44a126..ebe2589 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -26,6 +26,17 @@ export class PinStorage { SESSION_PIN = pin; } + public static resetSessionPIN(): void { + this.setSessionPIN(null); + } + + public static getSessionPin(): string { + if (SESSION_PIN == null) { + throw new Error("No session PIN available"); + } + return SESSION_PIN; + } + public static async getPinHash(): Promise { return new Promise(async (res, rej) => { chrome.storage.local.get({[PIN]: null}, async (resp) => { @@ -248,7 +259,7 @@ export class PSKStorage { return recoveryKeys.filter(x => x.backupKeyId === backupKeyId).length > 0 } - public static async loadRecoveryKeys(): Promise { + public static async loadRecoveryKeys(privateKeyImport: boolean = true): Promise { log.debug(`Loading recovery keys`); return new Promise(async (res, rej) => { chrome.storage.local.get({[RECOVERY_KEY]: null}, async (resp) => { @@ -268,7 +279,7 @@ export class PSKStorage { const recKeys = new Array(); for (let i = 0; i < exportJSON.length; ++i) { const json = exportJSON[i]; - const prvKey = await importKey(json.privKey); + const prvKey = privateKeyImport ? await importKey(json.privKey) : null; const pubKey = await window.crypto.subtle.importKey( 'jwk', json.pubKey, @@ -293,7 +304,7 @@ export class PSKStorage { export class CredentialsMap { public static async put(rpId: string, credSrc: PublicKeyCredentialSource): Promise { log.debug(`Storing credential map entry for ${rpId}`); - const mapEntryExists = await this.exists(rpId); + const mapEntryExists = await this.rpEntryExists(rpId); let credSrcs: PublicKeyCredentialSource[]; if (mapEntryExists) { log.debug('Credential map entry does already exist. Update entry.'); @@ -325,7 +336,9 @@ export class CredentialsMap { }); } - public static async load(rpId: string): Promise { + + + public static async load(rpId: string, keyImport: boolean = true): Promise { log.debug(`Loading credential map entry for ${rpId}`); return new Promise(async (res, rej) => { chrome.storage.local.get({[rpId]: null}, async (resp) => { @@ -343,7 +356,7 @@ export class CredentialsMap { const exportJSON = await JSON.parse(resp[rpId]); const credSrcs = new Array(); for (let i = 0; i < exportJSON.length; ++i) { - const credSrc = await PublicKeyCredentialSource.import(exportJSON[i]); + const credSrc = await PublicKeyCredentialSource.import(exportJSON[i], keyImport); credSrcs.push(credSrc); } log.debug('Loaded credential map entry successfully'); @@ -352,17 +365,17 @@ export class CredentialsMap { }); } - public static async lookup(rpId: string, credSrcId: string): Promise { - const credSrcs = await this.load(rpId); + public static async lookup(rpId: string, credSrcId: string, keyImport: boolean = true): Promise { + const credSrcs = await this.load(rpId, keyImport); const res = credSrcs.filter(x => x.id == credSrcId); if (res.length == 0) { return null; } else { return res[0]; } - } + }; - public static async exists(rpId: string): Promise { + public static async rpEntryExists(rpId: string): Promise { return new Promise(async (res, rej) => { chrome.storage.local.get({[rpId]: null}, async (resp) => { if (!!chrome.runtime.lastError) { @@ -378,11 +391,11 @@ export class CredentialsMap { } export class PublicKeyCredentialSource { - public static async import(json: any): Promise { + public static async import(json: any, keyImport: boolean = true): Promise { const _id = json.id; const _rpId = json.rpId; const _userHandle = json.userHandle; - const _privateKey = await importKey(json.privateKey); + const _privateKey = keyImport? await importKey(json.privateKey) : null; return new PublicKeyCredentialSource(_id, _privateKey, _rpId, _userHandle); } @@ -418,7 +431,7 @@ export class PublicKeyCredentialSource { async function exportKey(key: CryptoKey): Promise { const salt = window.crypto.getRandomValues(new Uint8Array(saltLength)); - const wrappingKey = await getWrappingKey(SESSION_PIN, salt); + const wrappingKey = await getWrappingKey(PinStorage.getSessionPin(), salt); const iv = window.crypto.getRandomValues(new Uint8Array(ivLength)); const wrapAlgorithm: AesGcmParams = { iv, @@ -457,7 +470,7 @@ async function importKey(rawKey: string): Promise { offset += keyAlgorithmByteLength; const keyBytes = keyPayload.subarray(offset); - const wrappingKey = await getWrappingKey(SESSION_PIN, salt); + const wrappingKey = await getWrappingKey(PinStorage.getSessionPin(), salt); const wrapAlgorithm: AesGcmParams = { iv, name: 'AES-GCM', diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 0b1b59c..09c7d49 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -54,14 +54,14 @@ export class Authenticator { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - const cred = await CredentialsMap.lookup(rpId, credId); + const cred = await CredentialsMap.lookup(rpId, credId, false); if (cred != null) { credentialOptions.push(cred); } } } else { // If no credentials were supplied, load all credentials associated to the RPID - credentialOptions = credentialOptions.concat(await CredentialsMap.load(rpId)); + credentialOptions = credentialOptions.concat(await CredentialsMap.load(rpId, false)); } if (credentialOptions.length == 0) { // Check if there is any recovery key that matches the provided credential descriptors @@ -69,7 +69,8 @@ export class Authenticator { for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - recKey = await RecoveryKey.findRecoveryKey(credId); + // lookup without private key import, because PIN not available yet + recKey = await RecoveryKey.findRecoveryKey(credId, false); if (recKey != null) { log.info('Recovery detected for', credId); break; @@ -80,23 +81,28 @@ export class Authenticator { throw new Error(`Container does not manage any related credentials`); } } - // Note: The authenticator won't let the user select a public key credential source - let credSource; - if (recKey == null) { // No recovery - credSource = credentialOptions[0]; - } const up = await userConsentCallback(); if (!up) { throw new Error(`no user consent`); } - // USer verification is always performed, because PIN is needed to decrypt keys + // User verification is always performed, because PIN is needed to decrypt keys let uv = await this.verifyUser("User verification is required."); if (!uv) { throw new Error(`user verification failed`); } + // Note: The authenticator won't let the user select a public key credential source + let credSource; + if (recKey == null) { // No recovery + // Load cred source again, now with private key, because UV provided PIN + credSource = await CredentialsMap.lookup(rpId, credentialOptions[0].id, true); + } else { + // Load recovery key again, now with private key, because UV provided PIn + recKey = await RecoveryKey.findRecoveryKey(recKey.backupKeyId, true) + } + // Step 8 let processedExtensions = undefined; if (extensions) { @@ -153,6 +159,8 @@ export class Authenticator { const prvKey = await ECDSA.fromKey(credSource.privateKey); const signature = await prvKey.sign(concatData); + PinStorage.resetSessionPIN(); + // Step 13 return new AssertionResponse(credSource.id, authenticatorData, signature, credSource.userHandle); } @@ -183,13 +191,14 @@ export class Authenticator { // Step 3 if (excludeCredentialDescriptorList) { // Simplified look up - const credMapEntries = await CredentialsMap.load(rpEntity.id); + // Load without key import, because PIN is not available yet + const credMapEntries = await CredentialsMap.load(rpEntity.id, false); for (let i = 0; i < excludeCredentialDescriptorList.length; i++) { const rawCredId = excludeCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); if (credMapEntries.findIndex(x => (x.id == credId) && (x.type === excludeCredentialDescriptorList[i].type)) >= 0) { - await userConsentCallback; + await userConsentCallback(); throw new Error(`authenticator manages credential of excludeCredentialDescriptorList`); } } @@ -209,7 +218,11 @@ export class Authenticator { throw new Error(`user verification failed`); } - return await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, up,undefined, extensions, userEntity.id); + const credential = await this.finishAuthenticatorMakeCredential(rpEntity.id, hash, uv, up,undefined, extensions, userEntity.id); + + PinStorage.resetSessionPIN(); + + return credential; } public static async finishAuthenticatorMakeCredential(rpId: string, hash: Uint8Array, uv: boolean, up:boolean, keyPair?: ICOSECompatibleKey, extensions?: Map, userHandle?: BufferSource): Promise<[string, Uint8Array]> { @@ -259,7 +272,6 @@ export class Authenticator { // Step 13 const attObj = await this.generateAttestationObject(hash, authenticatorData); - // Return value is not 1:1 WebAuthn conform log.debug('Created credential', credentialId) return [credentialId, attObj]; } @@ -376,6 +388,8 @@ export class Authenticator { if (match) { PinStorage.setSessionPIN(userPin); + } else { + alert("PIN does not match!"); } return match } diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index edbf40b..fe46b42 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -1,7 +1,7 @@ import * as axios from 'axios'; import * as CBOR from 'cbor'; -import {PSKStorage} from "./webauth_storage"; +import {PinStorage, PSKStorage} from "./webauth_storage"; import {getLogger} from "./logging"; import {base64ToByteArray, byteArrayToBase64} from "./utils"; import {ECDSA} from "./webauthn_crypto"; @@ -56,8 +56,8 @@ export class RecoveryKey { this.bdData = bdData; } - static async findRecoveryKey(backupKeyId: string): Promise { - const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.backupKeyId === backupKeyId); + static async findRecoveryKey(backupKeyId: string, importPrvKey: boolean = true): Promise { + const recoveryKeys = (await PSKStorage.loadRecoveryKeys(importPrvKey)).filter(x => x.backupKeyId === backupKeyId); if (recoveryKeys.length == 0) { return null } @@ -107,6 +107,7 @@ export class PSK { if (syncResponse.hasOwnProperty("recoveryOption")) { await PSK.pskRecoverySetup(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); } + PinStorage.resetSessionPIN(); }); } From 0b618e289cf75d38e4f360d2077f03393e31c6ee Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Tue, 27 Oct 2020 14:31:43 +0100 Subject: [PATCH 73/81] 1 PSK setup API endpoint --- src/webauthn_psk.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index fe46b42..38d8f08 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -90,7 +90,7 @@ export class PSK { const bdEndpoint = await PSKStorage.getBDEndpoint(); - return await axios.default.get(bdEndpoint + '/sync', {timeout: BD_TIMEOUT}) + return await axios.default.get(bdEndpoint + '/setup', {timeout: BD_TIMEOUT}) .then(async function(response) { log.debug(response); const syncResponse = response.data; @@ -135,7 +135,7 @@ export class PSK { let attCert = byteArrayToBase64(getAttestationCertificate(), true); - return await axios.default.post(bdEndpoint + '/recovery', { + return await axios.default.post(bdEndpoint + '/setup', { replacementKeys, attCert, delegatedAuthAlias, From 7c22b0e25cc5f6589ff99f2855efbf5c310fabd8 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 28 Oct 2020 12:44:50 +0100 Subject: [PATCH 74/81] Fix backup key storage --- src/webauth_storage.ts | 8 ++++---- src/webauthn_psk.ts | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index ebe2589..fba7029 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -158,7 +158,7 @@ export class PSKStorage { public static async storeBackupKeys(backupKeys: BackupKey[], bdUUID: string, override: boolean = false): Promise { log.debug(`Storing backup keys for`, bdUUID); - const backupKeysExists = await this.existBackupKeys(); + const backupKeysExists = await this.existBackupKeys(bdUUID); if (backupKeysExists && !override) { log.debug('Backup keys already exist. Update entry.'); const entries = await this.loadBackupKeys(bdUUID); @@ -202,15 +202,15 @@ export class PSKStorage { }); } - private static async existBackupKeys(): Promise { + private static async existBackupKeys(bdUUID: string): Promise { return new Promise(async (res, rej) => { - chrome.storage.local.get({[BACKUP_KEY]: null}, async (resp) => { + chrome.storage.local.get({[BACKUP_KEY + '_' + bdUUID]: null}, async (resp) => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.existBackupKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); return; } else { - res(!(resp[BACKUP_KEY] == null)); + res(!(resp[BACKUP_KEY + '_' + bdUUID] == null)); } }); }); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 38d8f08..94bfea6 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -152,8 +152,6 @@ export class PSK { const replacementKeyId = rawDelegations[i].replacementKeyId; const bdData = rawDelegations[i].bdData; - log.debug(rawDelegations[i]); - const keyPair = rawRecKeys.filter((x, _) => x[0] == replacementKeyId); if (keyPair.length !== 1) { log.warn('BD response does not contain delegation for key pair', replacementKeyId); From e70ea55075e4986b08a7dafee5862c09fbf8d0d6 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 28 Oct 2020 14:01:24 +0100 Subject: [PATCH 75/81] Fast lookup for recovery keys --- src/webauth_storage.ts | 52 +++++++++++++++++++++++------------ src/webauthn_authenticator.ts | 18 ++++++------ src/webauthn_psk.ts | 11 ++++---- tsconfig.json | 4 +-- 4 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/webauth_storage.ts b/src/webauth_storage.ts index fba7029..49118f6 100644 --- a/src/webauth_storage.ts +++ b/src/webauth_storage.ts @@ -216,13 +216,25 @@ export class PSKStorage { }); }; + public static async removeRecoveryKey(recoveryKey: RecoveryKey): Promise { + return new Promise(async (res, rej) => { + chrome.storage.local.remove([RECOVERY_KEY + "_" + recoveryKey.backupKeyId], () => { + if (!!chrome.runtime.lastError) { + log.error('Could not perform PSKStorage.removeRecoveryKey', chrome.runtime.lastError.message); + rej(chrome.runtime.lastError); + return; + } else { + res(); + } + }); + }) + } + public static async storeRecoveryKeys(recoveryKeys: RecoveryKey[]): Promise { log.debug('Storing recovery keys'); - recoveryKeys = recoveryKeys.concat(await this.loadRecoveryKeys()); - // Export recoveryKeys - const exportKeys = [] + const exportKeys = new Map(); for (let i = 0; i < recoveryKeys.length; i++) { const recKey = recoveryKeys[i]; const expPrvKey = await exportKey(recKey.privKey); @@ -236,12 +248,12 @@ export class PSKStorage { bdData: recKey.bdData, } - exportKeys.push(json) + exportKeys.set(RECOVERY_KEY + "_" + recKey.backupKeyId, JSON.stringify(json)) } - let exportJSON = JSON.stringify(exportKeys); + let storageObject = Object.fromEntries(exportKeys); return new Promise(async (res, rej) => { - chrome.storage.local.set({[RECOVERY_KEY]: exportJSON}, () => { + chrome.storage.local.set(storageObject, () => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.storeRecoveryKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); @@ -255,30 +267,34 @@ export class PSKStorage { public static async recoveryKeyExists(backupKeyId: string): Promise { log.debug('recoveryKeyExists: Requested backup key ID', backupKeyId); - const recoveryKeys = await PSKStorage.loadRecoveryKeys(); + const recoveryKeys = await PSKStorage.loadRecoveryKeys([backupKeyId]); return recoveryKeys.filter(x => x.backupKeyId === backupKeyId).length > 0 } - public static async loadRecoveryKeys(privateKeyImport: boolean = true): Promise { + public static async loadRecoveryKeys(backupKeyIds: string[], privateKeyImport: boolean = true): Promise { log.debug(`Loading recovery keys`); + const storageEntries = new Map(); + for (let i = 0; i < backupKeyIds.length; i++) { + storageEntries.set(RECOVERY_KEY + "_" + backupKeyIds[i], null); + } + let storageObject = Object.fromEntries(storageEntries); return new Promise(async (res, rej) => { - chrome.storage.local.get({[RECOVERY_KEY]: null}, async (resp) => { + chrome.storage.local.get(storageObject, async (resp) => { if (!!chrome.runtime.lastError) { log.error('Could not perform PSKStorage.loadRecoveryKeys', chrome.runtime.lastError.message); rej(chrome.runtime.lastError); return; } - if (resp[RECOVERY_KEY] == null) { - log.warn(`No recovery keys found`); - res([]); - return; - } - - const exportJSON = await JSON.parse(resp[RECOVERY_KEY]); const recKeys = new Array(); - for (let i = 0; i < exportJSON.length; ++i) { - const json = exportJSON[i]; + const storageKeys = Array.from(storageEntries.keys()); + for (let i = 0; i < storageKeys.length; i++) { + const storageKey = storageKeys[i]; + if (resp[storageKey] == null) { + continue; + } + + const json =await JSON.parse(resp[storageKey]); const prvKey = privateKeyImport ? await importKey(json.privKey) : null; const pubKey = await window.crypto.subtle.importKey( 'jwk', diff --git a/src/webauthn_authenticator.ts b/src/webauthn_authenticator.ts index 09c7d49..eee3e97 100644 --- a/src/webauthn_authenticator.ts +++ b/src/webauthn_authenticator.ts @@ -66,17 +66,17 @@ export class Authenticator { if (credentialOptions.length == 0) { // Check if there is any recovery key that matches the provided credential descriptors log.debug('No directly managed credentials found'); + const backupKeyIds = []; for (let i = 0; i < allowCredentialDescriptorList.length; i++) { const rawCredId = allowCredentialDescriptorList[i].id as ArrayBuffer; const credId = byteArrayToBase64(new Uint8Array(rawCredId), true); - // lookup without private key import, because PIN not available yet - recKey = await RecoveryKey.findRecoveryKey(credId, false); - if (recKey != null) { - log.info('Recovery detected for', credId); - break; - } + backupKeyIds.push(credId); } - if (recKey == null) { + // lookup without private key import, because PIN not available yet + recKey = await RecoveryKey.findRecoveryKey(backupKeyIds, false); + if (recKey != null) { + log.info('Recovery detected for backup key', recKey.backupKeyId); + } else { // No recovery and no associated credential found throw new Error(`Container does not manage any related credentials`); } @@ -99,8 +99,8 @@ export class Authenticator { // Load cred source again, now with private key, because UV provided PIN credSource = await CredentialsMap.lookup(rpId, credentialOptions[0].id, true); } else { - // Load recovery key again, now with private key, because UV provided PIn - recKey = await RecoveryKey.findRecoveryKey(recKey.backupKeyId, true) + // Load recovery key again, now with private key, because UV provided PIN + recKey = await RecoveryKey.findRecoveryKey([recKey.backupKeyId], true) } // Step 8 diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 94bfea6..083be3b 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -56,8 +56,8 @@ export class RecoveryKey { this.bdData = bdData; } - static async findRecoveryKey(backupKeyId: string, importPrvKey: boolean = true): Promise { - const recoveryKeys = (await PSKStorage.loadRecoveryKeys(importPrvKey)).filter(x => x.backupKeyId === backupKeyId); + static async findRecoveryKey(backupKeyIds: string[], importPrvKey: boolean = true): Promise { + const recoveryKeys = await PSKStorage.loadRecoveryKeys(backupKeyIds, importPrvKey); if (recoveryKeys.length == 0) { return null } @@ -65,9 +65,8 @@ export class RecoveryKey { return recoveryKeys[0]; } - static async removeRecoveryKey(backupKeyId: string): Promise { - const recoveryKeys = (await PSKStorage.loadRecoveryKeys()).filter(x => x.backupKeyId !== backupKeyId); - return await PSKStorage.storeRecoveryKeys(recoveryKeys); + static async removeRecoveryKey(recKey: RecoveryKey): Promise { + return await PSKStorage.removeRecoveryKey(recKey); } } @@ -195,7 +194,7 @@ export class PSK { log.debug('BDData', recoveryKey.bdData); // Finally remove recovery key since PSK output was generated successfully - await RecoveryKey.removeRecoveryKey(recoveryKey.backupKeyId); + await RecoveryKey.removeRecoveryKey(recoveryKey); const recoveryMessage = {attestationObject: rawAttObj, oldBackupKeyId: base64ToByteArray(recoveryKey.backupKeyId, true), delegationSignature: base64ToByteArray(recoveryKey.delegationSignature, true), bdData: base64ToByteArray(recoveryKey.bdData, true)} return [credentialId, recoveryMessage] diff --git a/tsconfig.json b/tsconfig.json index 33775b3..fc89866 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es6", + "target": "esnext", "module": "commonjs", "sourceMap": true, "esModuleInterop": true, @@ -11,7 +11,7 @@ "typeRoots": [ "node_modules/@types", "node_modules/web-ext-types", - "types", + "types" ] } } \ No newline at end of file From af1707107cf6b1451de2c3e9f510ef901ef7dc51 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 28 Oct 2020 19:49:23 +0100 Subject: [PATCH 76/81] Recovery Setup Response key match simplified --- src/webauthn_client.ts | 12 ++++++++---- src/webauthn_psk.ts | 12 ++++++------ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index 7c39468..dea61e7 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -18,6 +18,14 @@ export async function createPublicKeyCredential(origin: string, options: Credent throw new Error('options missing'); } + if (options.publicKey.attestation === 'none') { // Currently only direct and indirect attestation is supported + throw new Error('Client does not support none attestation'); + } + + if (options.publicKey.authenticatorSelection.authenticatorAttachment === 'cross-platform') { + throw new Error('Client does not support cross-platform authenticators'); + } + // Step 2 if (!sameOriginWithAncestors) { throw new Error(`sameOriginWithAncestors has to be true`); @@ -87,10 +95,6 @@ export async function createPublicKeyCredential(origin: string, options: Credent log.debug('Received attestation object'); - if (options.publicKey.attestation === 'none') { // Currently only direct and indirect attestation is supported - throw new Error('Client does not support none attestation'); - } - return { getClientExtensionResults: () => (clientExtensions), id: credentialId, diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 083be3b..e05013d 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -115,7 +115,7 @@ export class PSK { const bdEndpoint = await PSKStorage.getBDEndpoint(); - let rawRecKeys = new Array<[string, CryptoKeyPair]>() + let rawRecKeys = new Map() let replacementKeys = [] for (let i = 0; i < keyAmount; i++) { const keyPair = await window.crypto.subtle.generateKey( @@ -123,7 +123,7 @@ export class PSK { true, ['sign'], ); - rawRecKeys.push([i.toString(), keyPair]); + rawRecKeys.set(i.toString(), keyPair); // Prepare delegation request const pubKey = await ECDSA.fromKey(keyPair.publicKey); @@ -151,14 +151,14 @@ export class PSK { const replacementKeyId = rawDelegations[i].replacementKeyId; const bdData = rawDelegations[i].bdData; - const keyPair = rawRecKeys.filter((x, _) => x[0] == replacementKeyId); - if (keyPair.length !== 1) { + const keyPair = rawRecKeys.get(replacementKeyId) + if (!keyPair) { log.warn('BD response does not contain delegation for key pair', replacementKeyId); continue; } - const pubKey = keyPair[0][1].publicKey; - const privKey = keyPair[0][1].privateKey; + const pubKey = keyPair.publicKey; + const privKey = keyPair.privateKey; const recoveryKey = new RecoveryKey(backupKeyId, pubKey, privKey, sign, bdData) From e04348ea8a6affc4ba5fda49df35a7e627e0e342 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Wed, 28 Oct 2020 20:27:31 +0100 Subject: [PATCH 77/81] Fix authenticatorAttachment bug --- src/webauthn_client.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/webauthn_client.ts b/src/webauthn_client.ts index dea61e7..aa38cf8 100644 --- a/src/webauthn_client.ts +++ b/src/webauthn_client.ts @@ -22,10 +22,6 @@ export async function createPublicKeyCredential(origin: string, options: Credent throw new Error('Client does not support none attestation'); } - if (options.publicKey.authenticatorSelection.authenticatorAttachment === 'cross-platform') { - throw new Error('Client does not support cross-platform authenticators'); - } - // Step 2 if (!sameOriginWithAncestors) { throw new Error(`sameOriginWithAncestors has to be true`); @@ -65,7 +61,7 @@ export async function createPublicKeyCredential(origin: string, options: Credent let userVerification = true; let residentKey = false; if (options.publicKey.authenticatorSelection) { - if (options.publicKey.authenticatorSelection.authenticatorAttachment && (options.publicKey.authenticatorSelection.authenticatorAttachment !== 'platform')) { + if ((options.publicKey.authenticatorSelection.authenticatorAttachment != undefined) && (options.publicKey.authenticatorSelection.authenticatorAttachment !== 'platform')) { throw new Error(`${options.publicKey.authenticatorSelection.authenticatorAttachment} authenticator requested, but only platform authenticators available`); } From 0a4c5c5a8a6adf2463ed02c1ae5a016cb99181af Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Thu, 29 Oct 2020 18:41:43 +0100 Subject: [PATCH 78/81] PSK error log --- src/webauthn_psk.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index e05013d..9ad085c 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -107,6 +107,9 @@ export class PSK { await PSK.pskRecoverySetup(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); } PinStorage.resetSessionPIN(); + }).catch(e => { + alert('PSK Initial Setup Failed!'); + log.error(e); }); } @@ -165,8 +168,11 @@ export class PSK { recoveryKeys.push(recoveryKey); } - log.debug('Recovery finished. Recovery keys:', recoveryKeys); + log.debug('Recovery Setup finished. Recovery keys:', recoveryKeys); await PSKStorage.storeRecoveryKeys(recoveryKeys); + }).catch(e => { + alert('PSK Recovery Setup Failed!'); + log.error(e); }); } From 5c69c6e7e725aaad3b1fd47b9a5c36318f3fa681 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Fri, 13 Nov 2020 11:26:57 +0100 Subject: [PATCH 79/81] Fix logging --- src/background.ts | 2 +- src/webauthn_psk.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/background.ts b/src/background.ts index 23d2f0f..d882b12 100644 --- a/src/background.ts +++ b/src/background.ts @@ -118,7 +118,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { getCredential(msg, sender).then(sendResponse); break; case 'psk_setup': - pskSync().then(() => alert('PSK setup flow was successful.'), e => log.error('PSK setup flow failed', { errorType: `${(typeof e)}` }, e)); + pskSync().then(() => {}, e => log.error('PSK setup flow failed', { errorType: `${(typeof e)}` }, e)); break; case 'psk_options': pskOptions(msg.alias, msg.url).then(() => alert('PSK options set successfully.'), e => log.error('failed to set psk options', { errorType: `${(typeof e)}` }, e)); diff --git a/src/webauthn_psk.ts b/src/webauthn_psk.ts index 9ad085c..7ee9a8d 100644 --- a/src/webauthn_psk.ts +++ b/src/webauthn_psk.ts @@ -106,11 +106,11 @@ export class PSK { if (syncResponse.hasOwnProperty("recoveryOption")) { await PSK.pskRecoverySetup(syncResponse.authAlias, syncResponse.recoveryOption.originAuthAlias, syncResponse.recoveryOption.keyAmount); } - PinStorage.resetSessionPIN(); + alert('PSK setup flow was successful.') }).catch(e => { alert('PSK Initial Setup Failed!'); log.error(e); - }); + }).finally(() => PinStorage.resetSessionPIN()); } private static async pskRecoverySetup(delegatedAuthAlias: string, originAuthAlias: string, keyAmount: number): Promise { From 12aa653438c3e54e375afaf5159c331b9b20532b Mon Sep 17 00:00:00 2001 From: Blobonat Date: Tue, 17 Nov 2020 21:56:37 +0100 Subject: [PATCH 80/81] Add OS Support section --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 86c4617..3fba0e0 100644 --- a/README.md +++ b/README.md @@ -27,4 +27,10 @@ You can load the project as an unpacked extension. Upon building, you may load t # PSK Protocol Support - Support for PSK WebAuthn Extension -- Support for PSK Setup API \ No newline at end of file +- Support for PSK Setup API + +# OS Support + +The extension was tested with Ubuntu 18.04 and macOS 10.15.7 + +Windows 10 does not use the default Chrome popup to search for available authenticators, but uses a Windows Security popup that unfortunately disables the Chrome window until a certain timeout. Once the timeout is reached and the Chrome browser window becomes active again, you can interact the cKey extension. From d9ee89939149df24ce309bc2bb2d055221a50eb7 Mon Sep 17 00:00:00 2001 From: Maximilian Leibl Date: Sun, 10 Jan 2021 14:51:59 +0100 Subject: [PATCH 81/81] Use PSK extension identifier with capital letters --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index bd1e695..b9bd5e4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -21,7 +21,7 @@ export const ES256 = 'P-256'; export const SHA256_COSE = 1; export const PIN = 'pin'; -export const PSK_EXTENSION_IDENTIFIER = 'psk'; +export const PSK_EXTENSION_IDENTIFIER = 'PSK'; export const BACKUP_KEY = 'backup_key'; export const BD_ENDPOINT = 'bd_endpoint'; export const DEFAULT_BD_ENDPOINT = 'http://localhost:8005';