@bedrock/kms-module-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +115 -0
- package/README.md +3 -0
- package/lib/KmsModule.js +176 -0
- package/lib/core/Core.js +226 -0
- package/lib/core/aeskw.js +19 -0
- package/lib/core/asymmetricKey.js +145 -0
- package/lib/core/hmacKey.js +99 -0
- package/lib/core/index.js +4 -0
- package/lib/core/keyAgreementKey.js +148 -0
- package/lib/core/operations.js +42 -0
- package/lib/core/wrapKey.js +89 -0
- package/lib/index.js +52 -0
- package/package.json +53 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Bedrock Non-Commercial License v1.0
|
|
2
|
+
===================================
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2011-2016 Digital Bazaar, Inc.
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Summary
|
|
8
|
+
=======
|
|
9
|
+
|
|
10
|
+
This license allows the licensee to use Bedrock and its software modules
|
|
11
|
+
for non-commercial purposes such as self-study, research, personal
|
|
12
|
+
projects, or for evaluation purposes. If the licensee uses Bedrock
|
|
13
|
+
directly or indirectly to generate revenue, or to provide products or
|
|
14
|
+
services to more than 500 people (users), the licensee must immediately
|
|
15
|
+
obtain a non-profit or commercial license.
|
|
16
|
+
|
|
17
|
+
Examples
|
|
18
|
+
========
|
|
19
|
+
|
|
20
|
+
These are examples of cases that are allowed by this license:
|
|
21
|
+
|
|
22
|
+
* The licensee is an individual that creates Bedrock-dependent software for
|
|
23
|
+
personal use only.
|
|
24
|
+
* The licensee is an individual or group of students/researchers that uses
|
|
25
|
+
Bedrock to experiment with an idea for a non-commercial project.
|
|
26
|
+
* The licensee is a startup company that prototypes a Bedrock-dependent
|
|
27
|
+
product before they have cash flow and will be testing the prototype
|
|
28
|
+
software with less than 500 users. The service will not generate revenue
|
|
29
|
+
of any kind.
|
|
30
|
+
* The licensee is a for-profit organization that creates a product or
|
|
31
|
+
service that is used by less than 500 users and is built with or
|
|
32
|
+
integrates with Bedrock. The service must be exclusively provided for free
|
|
33
|
+
and no parent, subsidiary, agent, or affiliate organization may profit
|
|
34
|
+
from its use.
|
|
35
|
+
|
|
36
|
+
These cases require a non-profit or commercial license:
|
|
37
|
+
|
|
38
|
+
* The licensee is a non-profit that receives funding to create and/or run a
|
|
39
|
+
Bedrock-dependent service.
|
|
40
|
+
* The licensee is a startup company with Bedrock-dependent software that is
|
|
41
|
+
funded by another organization.
|
|
42
|
+
* The licensee is a startup company that is going into production with
|
|
43
|
+
Bedrock-dependent software.
|
|
44
|
+
* The licensee has more than 500 users using a Bedrock-dependent service
|
|
45
|
+
either directly or indirectly.
|
|
46
|
+
* The licensee is a medium to large organization that builds or integrates a
|
|
47
|
+
commercial product or service with Bedrock.
|
|
48
|
+
|
|
49
|
+
THE LICENSE
|
|
50
|
+
===========
|
|
51
|
+
|
|
52
|
+
This section and all subsequent sections of this document constitute the
|
|
53
|
+
agreement between the licensee and Digital Bazaar, Inc.
|
|
54
|
+
|
|
55
|
+
DEFINITIONS
|
|
56
|
+
===========
|
|
57
|
+
|
|
58
|
+
* Product - The Bedrock software and any modules associated with Bedrock
|
|
59
|
+
where Digital Bazaar, Inc. owns the copyright.
|
|
60
|
+
|
|
61
|
+
CONDITIONS
|
|
62
|
+
==========
|
|
63
|
+
|
|
64
|
+
Redistribution and use in source and binary forms, with or without
|
|
65
|
+
modification, are permitted for NON-COMMERCIAL PURPOSES as long as the
|
|
66
|
+
following conditions are met:
|
|
67
|
+
|
|
68
|
+
1. Any use of the Product must not generate revenue for the licensee or
|
|
69
|
+
any parent, subsidiary, agent, or affiliate of the licensee. Use of
|
|
70
|
+
Product includes, but is not limited to, interacting with any of the
|
|
71
|
+
licensee's Product-dependent products or services over a network.
|
|
72
|
+
|
|
73
|
+
2. The aggregate number of individual people (users) of the licensee's
|
|
74
|
+
products or services that use Product must be less than 500.
|
|
75
|
+
|
|
76
|
+
3. Redistributions of source code must retain the above copyright notice
|
|
77
|
+
intact, this list of conditions and the following disclaimer.
|
|
78
|
+
|
|
79
|
+
4. Redistributions in binary form must reproduce the above copyright
|
|
80
|
+
notice, this license and the following disclaimer in the documentation and
|
|
81
|
+
on a web page available via interactive use and/or other materials
|
|
82
|
+
provided with the distribution.
|
|
83
|
+
|
|
84
|
+
5. Neither the name of the copyright holder, the names of its
|
|
85
|
+
contributors, nor any trademarks held by the copyright holder may be used
|
|
86
|
+
to endorse or promote products or services built using the Product without
|
|
87
|
+
specific prior written permission.
|
|
88
|
+
|
|
89
|
+
6. Any modifications are clearly outlined in release documentation and are
|
|
90
|
+
specifically mentioned as not being a part of an official Product release.
|
|
91
|
+
No additional restrictions to this license may be made when distributing
|
|
92
|
+
modifications.
|
|
93
|
+
|
|
94
|
+
7. For the avoidance of doubt, this license prohibits sublicensing of the
|
|
95
|
+
Product.
|
|
96
|
+
|
|
97
|
+
8. Any breach of this license by licensee must be resolved within 30 days.
|
|
98
|
+
Failure to do so results in the termination of this license.
|
|
99
|
+
|
|
100
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
101
|
+
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
102
|
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
103
|
+
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
104
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
105
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
106
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
107
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
108
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
109
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
110
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
111
|
+
|
|
112
|
+
To obtain a non-profit or commercial license for Product, please contact
|
|
113
|
+
Digital Bazaar, Inc. at the following email address:
|
|
114
|
+
|
|
115
|
+
Digital Bazaar <support@digitalbazaar.com>
|
package/README.md
ADDED
package/lib/KmsModule.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// provides main common interface to be called by a WebKMS system
|
|
6
|
+
export class KmsModule {
|
|
7
|
+
constructor({core, keyStorage} = {}) {
|
|
8
|
+
this.core = core;
|
|
9
|
+
this.keyStorage = keyStorage;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a new key.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} options - The options to use.
|
|
16
|
+
* @param {string} options.keyId - The key ID to use.
|
|
17
|
+
* @param {string} options.controller - The key controller.
|
|
18
|
+
* @param {object} options.operation - The KMS operation.
|
|
19
|
+
*
|
|
20
|
+
* @returns {Promise<object>} Key information `{keyId, keyDescription}`.
|
|
21
|
+
*/
|
|
22
|
+
async generateKey({keyId, controller, operation} = {}) {
|
|
23
|
+
const {core, keyStorage} = this;
|
|
24
|
+
const {key, keyDescription} = await core.generateKey({
|
|
25
|
+
keyId, controller, operation
|
|
26
|
+
});
|
|
27
|
+
await keyStorage.insert({key});
|
|
28
|
+
|
|
29
|
+
return {keyId, keyDescription};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the number of keys in a given keystore.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} options - The options to use.
|
|
36
|
+
* @param {string} options.keystoreId - The ID of the keystore.
|
|
37
|
+
*
|
|
38
|
+
* @returns {Promise<object>} Key count information.
|
|
39
|
+
*/
|
|
40
|
+
async getKeyCount({keystoreId} = {}) {
|
|
41
|
+
const {keyStorage} = this;
|
|
42
|
+
return keyStorage.getCount({keystoreId});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Gets the key description (no private key material) for the given key.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} options - The options to use.
|
|
49
|
+
* @param {string} options.keyId - The key ID to use.
|
|
50
|
+
* @param {string} options.controller - The key controller.
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<object>} Key information.
|
|
53
|
+
*/
|
|
54
|
+
async getKeyDescription({keyId, controller} = {}) {
|
|
55
|
+
const {core, keyStorage} = this;
|
|
56
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
57
|
+
return core.getKeyDescription({key, controller});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wraps a cryptographic key using a key encryption key (KEK).
|
|
62
|
+
*
|
|
63
|
+
* @param {object} options - The options to use.
|
|
64
|
+
* @param {string} options.keyId - The key ID to use.
|
|
65
|
+
* @param {object} options.operation - The KMS operation.
|
|
66
|
+
* @param {object} [options.zcapInvocation] - The zcap invocation used to
|
|
67
|
+
* run the KMS operation; if the KMS operation was invoked via zcap.
|
|
68
|
+
*
|
|
69
|
+
* @returns {Promise<object>} An object containing `{wrappedKey}`.
|
|
70
|
+
*/
|
|
71
|
+
async wrapKey({keyId, operation, zcapInvocation} = {}) {
|
|
72
|
+
const {core, keyStorage} = this;
|
|
73
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
74
|
+
_checkZcapInvocationRules({key, zcapInvocation});
|
|
75
|
+
return core.wrapKey({key, operation});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Unwraps a cryptographic key using a key encryption key (KEK).
|
|
80
|
+
*
|
|
81
|
+
* @param {object} options - The options to use.
|
|
82
|
+
* @param {string} options.keyId - The key ID to use.
|
|
83
|
+
* @param {object} options.operation - The KMS operation.
|
|
84
|
+
* @param {object} [options.zcapInvocation] - The zcap invocation used to
|
|
85
|
+
* run the KMS operation; if the KMS operation was invoked via zcap.
|
|
86
|
+
*
|
|
87
|
+
* @returns {Promise<object>} An object containing `{unwrappedKey}`.
|
|
88
|
+
*/
|
|
89
|
+
async unwrapKey({keyId, operation, zcapInvocation} = {}) {
|
|
90
|
+
const {core, keyStorage} = this;
|
|
91
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
92
|
+
_checkZcapInvocationRules({key, zcapInvocation});
|
|
93
|
+
return core.unwrapKey({key, operation});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Signs some data. Note that the data will be sent to the server, so if
|
|
98
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
99
|
+
* hashing the data first may present interoperability issues so choose
|
|
100
|
+
* wisely.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} options - The options to use.
|
|
103
|
+
* @param {string} options.keyId - The key ID to use.
|
|
104
|
+
* @param {object} options.operation - The KMS operation.
|
|
105
|
+
* @param {object} [options.zcapInvocation] - The zcap invocation used to
|
|
106
|
+
* run the KMS operation; if the KMS operation was invoked via zcap.
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<object>} An object containing `{signatureValue}`.
|
|
109
|
+
*/
|
|
110
|
+
async sign({keyId, operation, zcapInvocation} = {}) {
|
|
111
|
+
const {core, keyStorage} = this;
|
|
112
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
113
|
+
_checkZcapInvocationRules({key, zcapInvocation});
|
|
114
|
+
return core.sign({key, operation});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Verifies some data. Note that the data will be sent to the server, so if
|
|
119
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
120
|
+
* hashing the data first may present interoperability issues so choose
|
|
121
|
+
* wisely.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} options - The options to use.
|
|
124
|
+
* @param {string} options.keyId - The key ID to use.
|
|
125
|
+
* @param {object} options.operation - The KMS operation.
|
|
126
|
+
* @param {object} [options.zcapInvocation] - The zcap invocation used to
|
|
127
|
+
* run the KMS operation; if the KMS operation was invoked via zcap.
|
|
128
|
+
*
|
|
129
|
+
* @returns {Promise<object>} An object containing `{verified}`.
|
|
130
|
+
*/
|
|
131
|
+
async verify({keyId, operation, zcapInvocation} = {}) {
|
|
132
|
+
const {core, keyStorage} = this;
|
|
133
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
134
|
+
_checkZcapInvocationRules({key, zcapInvocation});
|
|
135
|
+
return core.verify({key, operation});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Derives a shared secret via the given peer public key, typically for use
|
|
140
|
+
* as one parameter for computing a shared key. It should not be used as
|
|
141
|
+
* a shared key itself, but rather input into a key derivation function (KDF)
|
|
142
|
+
* to produce a shared key.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} options - The options to use.
|
|
145
|
+
* @param {string} options.keyId - The key ID to use.
|
|
146
|
+
* @param {object} options.operation - The KMS operation.
|
|
147
|
+
* @param {object} [options.zcapInvocation] - The zcap invocation used to
|
|
148
|
+
* run the KMS operation; if the KMS operation was invoked via zcap.
|
|
149
|
+
*
|
|
150
|
+
* @returns {Promise<object>} An object containing `{secret}`.
|
|
151
|
+
*/
|
|
152
|
+
async deriveSecret({keyId, operation, zcapInvocation} = {}) {
|
|
153
|
+
const {core, keyStorage} = this;
|
|
154
|
+
const {key} = await keyStorage.get({id: keyId});
|
|
155
|
+
_checkZcapInvocationRules({key, zcapInvocation});
|
|
156
|
+
return core.deriveSecret({key, operation});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _checkZcapInvocationRules({key, zcapInvocation}) {
|
|
161
|
+
// operation not invoked via zcap
|
|
162
|
+
if(!zcapInvocation) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
// no extra zcap invocation restrictions on the key
|
|
166
|
+
if(key.maxCapabilityChainLength === undefined) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// ensure zcap invocation capability change length does not exceed the
|
|
170
|
+
// rules from the key record
|
|
171
|
+
if(zcapInvocation.dereferencedChain.length > key.maxCapabilityChainLength) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
'Maximum zcap invocation capability chain length ' +
|
|
174
|
+
`(${key.maxCapabilityChainLength}) exceeded.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
package/lib/core/Core.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import assert from 'assert-plus';
|
|
5
|
+
import {getKeyOp} from './operations.js';
|
|
6
|
+
import {parseTemplate} from 'url-template';
|
|
7
|
+
|
|
8
|
+
export class Core {
|
|
9
|
+
constructor() {}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a new key.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} options - The options to use.
|
|
15
|
+
* @param {string} options.keyId - The key ID to use.
|
|
16
|
+
* @param {string} options.controller - The key controller.
|
|
17
|
+
* @param {object} options.operation - The KMS operation.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<object>} Key information.
|
|
20
|
+
*/
|
|
21
|
+
async generateKey({keyId, controller, operation}) {
|
|
22
|
+
assert.string(keyId, 'options.keyId');
|
|
23
|
+
assert.string(controller, 'options.controller');
|
|
24
|
+
assert.object(operation, 'options.operation');
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
invocationTarget: {
|
|
28
|
+
// specific key type
|
|
29
|
+
type,
|
|
30
|
+
// max acceptable length of the capability chain used in a capability
|
|
31
|
+
// invocation to invoke a KMS operation with the key
|
|
32
|
+
maxCapabilityChainLength,
|
|
33
|
+
// any public alias for the key
|
|
34
|
+
publicAlias,
|
|
35
|
+
// any public alias template for the key
|
|
36
|
+
publicAliasTemplate
|
|
37
|
+
}
|
|
38
|
+
} = operation;
|
|
39
|
+
assert.string(type, 'options.operation.invocationTarget.type');
|
|
40
|
+
assert.optionalNumber(
|
|
41
|
+
maxCapabilityChainLength,
|
|
42
|
+
'options.operation.invocationTarget.maxCapabilityChainLength');
|
|
43
|
+
assert.optionalString(
|
|
44
|
+
publicAlias, 'options.operation.invocationTarget.publicAlias');
|
|
45
|
+
assert.optionalString(
|
|
46
|
+
publicAliasTemplate,
|
|
47
|
+
'options.operation.invocationTarget.publicAliasTemplate');
|
|
48
|
+
|
|
49
|
+
if(publicAlias && publicAliasTemplate) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'Only one of "publicAlias" or "publicAliasTemplate" may be given.');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// if `publicAliasTemplate` was given, ensure it can be parsed prior to
|
|
55
|
+
// attempting key generation
|
|
56
|
+
let template;
|
|
57
|
+
if(publicAliasTemplate) {
|
|
58
|
+
template = parseTemplate(publicAliasTemplate);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// perform key generation
|
|
62
|
+
const op = getKeyOp({name: 'generateKey', type});
|
|
63
|
+
const {key, keyDescription} = await op({
|
|
64
|
+
keyId, type, controller,
|
|
65
|
+
maxCapabilityChainLength, publicAlias, publicAliasTemplate
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// add any extra key restrictions
|
|
69
|
+
if(maxCapabilityChainLength !== undefined) {
|
|
70
|
+
key.maxCapabilityChainLength = maxCapabilityChainLength;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// add any public alias or template
|
|
74
|
+
if(publicAlias) {
|
|
75
|
+
key.publicAlias = publicAlias;
|
|
76
|
+
// override public key `id` with `publicAlias`
|
|
77
|
+
keyDescription.id = publicAlias;
|
|
78
|
+
} else if(publicAliasTemplate) {
|
|
79
|
+
key.publicAliasTemplate = publicAliasTemplate;
|
|
80
|
+
// compute public alias from template
|
|
81
|
+
keyDescription.id = template.expand(keyDescription);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {keyId, key, keyDescription};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets the key description (no private key material) for the given key.
|
|
89
|
+
*
|
|
90
|
+
* @param {object} options - The options to use.
|
|
91
|
+
* @param {object} options.key - The key object to use.
|
|
92
|
+
* @param {string} options.controller - The key controller.
|
|
93
|
+
*
|
|
94
|
+
* @returns {Promise<object>} Key information.
|
|
95
|
+
*/
|
|
96
|
+
async getKeyDescription({key, controller} = {}) {
|
|
97
|
+
let type;
|
|
98
|
+
if(key.type.startsWith('urn:webkms:multikey:')) {
|
|
99
|
+
type = 'Multikey';
|
|
100
|
+
} else {
|
|
101
|
+
type = key.type;
|
|
102
|
+
}
|
|
103
|
+
const description = {
|
|
104
|
+
'@context': key['@context'],
|
|
105
|
+
id: key.id,
|
|
106
|
+
type,
|
|
107
|
+
controller
|
|
108
|
+
};
|
|
109
|
+
if(key.publicKeyMultibase) {
|
|
110
|
+
description.publicKeyMultibase = key.publicKeyMultibase;
|
|
111
|
+
}
|
|
112
|
+
if(key.publicKeyBase58) {
|
|
113
|
+
description.publicKeyBase58 = key.publicKeyBase58;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// override `id` with `publicAlias` / `publicAliasTemplate` if available
|
|
117
|
+
if(key.publicAlias) {
|
|
118
|
+
description.id = key.publicAlias;
|
|
119
|
+
} else if(key.publicAliasTemplate) {
|
|
120
|
+
// compute public alias from template
|
|
121
|
+
const template = parseTemplate(key.publicAliasTemplate);
|
|
122
|
+
description.id = template.expand(description);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return description;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Wraps a cryptographic key using a key encryption key (KEK).
|
|
130
|
+
*
|
|
131
|
+
* @param {object} options - The options to use.
|
|
132
|
+
* @param {object} options.key - The key object to use.
|
|
133
|
+
* @param {object} options.operation - The KMS operation.
|
|
134
|
+
*
|
|
135
|
+
* @returns {Promise<object>} An object containing `{wrappedKey}`.
|
|
136
|
+
*/
|
|
137
|
+
async wrapKey({key, operation}) {
|
|
138
|
+
assert.object(key, 'options.key');
|
|
139
|
+
assert.object(operation, 'options.operation');
|
|
140
|
+
|
|
141
|
+
const {type} = key;
|
|
142
|
+
const op = getKeyOp({name: 'wrapKey', type});
|
|
143
|
+
return op({kek: key, operation});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Unwraps a cryptographic key using a key encryption key (KEK).
|
|
148
|
+
*
|
|
149
|
+
* @param {object} options - The options to use.
|
|
150
|
+
* @param {object} options.key - The key object to use.
|
|
151
|
+
* @param {object} options.operation - The KMS operation.
|
|
152
|
+
*
|
|
153
|
+
* @returns {Promise<object>} An object containing `{unwrappedKey}`.
|
|
154
|
+
*/
|
|
155
|
+
async unwrapKey({key, operation}) {
|
|
156
|
+
assert.object(key, 'options.key');
|
|
157
|
+
assert.object(operation, 'options.operation');
|
|
158
|
+
|
|
159
|
+
const {type} = key;
|
|
160
|
+
const op = getKeyOp({name: 'unwrapKey', type});
|
|
161
|
+
return op({kek: key, operation});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Signs some data. Note that the data will be sent to the server, so if
|
|
166
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
167
|
+
* hashing the data first may present interoperability issues so choose
|
|
168
|
+
* wisely.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} options - The options to use.
|
|
171
|
+
* @param {object} options.key - The key to use.
|
|
172
|
+
* @param {object} options.operation - The KMS operation.
|
|
173
|
+
*
|
|
174
|
+
* @returns {Promise<object>} An object containing `{signatureValue}`.
|
|
175
|
+
*/
|
|
176
|
+
async sign({key, operation}) {
|
|
177
|
+
assert.object(key, 'options.key');
|
|
178
|
+
assert.object(operation, 'options.operation');
|
|
179
|
+
|
|
180
|
+
const {type} = key;
|
|
181
|
+
const op = getKeyOp({name: 'sign', type});
|
|
182
|
+
return op({key, operation});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Verifies some data. Note that the data will be sent to the server, so if
|
|
187
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
188
|
+
* hashing the data first may present interoperability issues so choose
|
|
189
|
+
* wisely.
|
|
190
|
+
*
|
|
191
|
+
* @param {object} options - The options to use.
|
|
192
|
+
* @param {object} options.key - The key to use.
|
|
193
|
+
* @param {object} options.operation - The KMS operation.
|
|
194
|
+
*
|
|
195
|
+
* @returns {Promise<object>} An object containing `{verified}`.
|
|
196
|
+
*/
|
|
197
|
+
async verify({key, operation}) {
|
|
198
|
+
assert.object(key, 'options.key');
|
|
199
|
+
assert.object(operation, 'options.operation');
|
|
200
|
+
|
|
201
|
+
const {type} = key;
|
|
202
|
+
const op = getKeyOp({name: 'verify', type});
|
|
203
|
+
return op({key, operation});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Derives a shared secret via the given peer public key, typically for use
|
|
208
|
+
* as one parameter for computing a shared key. It should not be used as
|
|
209
|
+
* a shared key itself, but rather input into a key derivation function (KDF)
|
|
210
|
+
* to produce a shared key.
|
|
211
|
+
*
|
|
212
|
+
* @param {object} options - The options to use.
|
|
213
|
+
* @param {object} options.key - The key to use.
|
|
214
|
+
* @param {object} options.operation - The KMS operation.
|
|
215
|
+
*
|
|
216
|
+
* @returns {Promise<object>} An object containing `{secret}`.
|
|
217
|
+
*/
|
|
218
|
+
async deriveSecret({key, operation}) {
|
|
219
|
+
assert.object(key, 'options.key');
|
|
220
|
+
assert.object(operation, 'options.operation');
|
|
221
|
+
|
|
222
|
+
const {type} = key;
|
|
223
|
+
const op = getKeyOp({name: 'deriveSecret', type});
|
|
224
|
+
return op({key, operation});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const AES_KW_ALGORITHM = 'id-aes256-wrap';
|
|
7
|
+
const AES_KW_RFC3394_IV = Buffer.from('A6A6A6A6A6A6A6A6', 'hex');
|
|
8
|
+
|
|
9
|
+
export async function unwrapKey({secretKey, wrapped} = {}) {
|
|
10
|
+
const decipher = crypto.createDecipheriv(
|
|
11
|
+
AES_KW_ALGORITHM, secretKey, AES_KW_RFC3394_IV);
|
|
12
|
+
return Buffer.concat([decipher.update(wrapped), decipher.final()]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function wrapKey({secretKey, unwrapped} = {}) {
|
|
16
|
+
const cipher = crypto.createCipheriv(
|
|
17
|
+
AES_KW_ALGORITHM, secretKey, AES_KW_RFC3394_IV);
|
|
18
|
+
return Buffer.concat([cipher.update(unwrapped), cipher.final()]);
|
|
19
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import * as base58 from 'base58-universal';
|
|
5
|
+
import * as bedrock from '@bedrock/core';
|
|
6
|
+
import * as Bls12381Multikey from '@digitalbazaar/bls12-381-multikey';
|
|
7
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
8
|
+
import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey';
|
|
9
|
+
|
|
10
|
+
const {util: {BedrockError}} = bedrock;
|
|
11
|
+
|
|
12
|
+
const ED25519_2018_V1_URL =
|
|
13
|
+
'https://w3id.org/security/suites/ed25519-2018/v1';
|
|
14
|
+
const ED25519_2020_V1_URL =
|
|
15
|
+
'https://w3id.org/security/suites/ed25519-2020/v1';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a new asymmetric key pair.
|
|
19
|
+
*
|
|
20
|
+
* @ignore
|
|
21
|
+
* @param {object} options - The options to use.
|
|
22
|
+
* @param {string} options.keyId - The key ID to use.
|
|
23
|
+
* @param {object} options.type - The key type.
|
|
24
|
+
* @param {string} options.controller - The key controller.
|
|
25
|
+
*
|
|
26
|
+
* @returns {Promise<object>} An object containing `{key, keyDescription}`.
|
|
27
|
+
*/
|
|
28
|
+
export async function generateKey({keyId, type, controller} = {}) {
|
|
29
|
+
let keyPair;
|
|
30
|
+
if(type.includes('Ed25519')) {
|
|
31
|
+
keyPair = await Ed25519Multikey.generate({id: keyId});
|
|
32
|
+
} else if(type.startsWith('urn:webkms:multikey:P-')) {
|
|
33
|
+
const curve = type.slice('urn:webkms:multikey:'.length);
|
|
34
|
+
keyPair = await EcdsaMultikey.generate({id: keyId, curve});
|
|
35
|
+
} else if(type.startsWith('urn:webkms:multikey:BBS-') ||
|
|
36
|
+
type === 'urn:webkms:multikey:Bls12381G2') {
|
|
37
|
+
let algorithm = type.slice('urn:webkms:multikey:'.length);
|
|
38
|
+
if(algorithm === 'Bls12381G2') {
|
|
39
|
+
// default curve-as-algorithm to:
|
|
40
|
+
algorithm = 'BBS-BLS12-381-SHA-256';
|
|
41
|
+
}
|
|
42
|
+
keyPair = await Bls12381Multikey.generateBbsKeyPair(
|
|
43
|
+
{id: keyId, algorithm});
|
|
44
|
+
} else {
|
|
45
|
+
throw new BedrockError(`Unsupported key type "${type}".`, {
|
|
46
|
+
name: 'NotSupportedError',
|
|
47
|
+
details: {public: true, httpStatusCode: 400}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// generate full key portion for internal record
|
|
52
|
+
let key = await keyPair.export(
|
|
53
|
+
{publicKey: true, secretKey: true, includeContext: true});
|
|
54
|
+
key.type = type;
|
|
55
|
+
|
|
56
|
+
// generate public key description
|
|
57
|
+
let keyDescription = await keyPair.export({
|
|
58
|
+
publicKey: true, includeContext: true
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// perform transform for legacy key types
|
|
62
|
+
if(type === 'Ed25519VerificationKey2020') {
|
|
63
|
+
key = {
|
|
64
|
+
'@context': ED25519_2020_V1_URL,
|
|
65
|
+
id: key.id,
|
|
66
|
+
type,
|
|
67
|
+
privateKeyMultibase: key.secretKeyMultibase,
|
|
68
|
+
publicKeyMultibase: key.publicKeyMultibase
|
|
69
|
+
};
|
|
70
|
+
keyDescription['@context'] = ED25519_2020_V1_URL;
|
|
71
|
+
keyDescription.type = type;
|
|
72
|
+
} else if(type === 'Ed25519VerificationKey2018') {
|
|
73
|
+
key = {
|
|
74
|
+
'@context': ED25519_2018_V1_URL,
|
|
75
|
+
id: key.id,
|
|
76
|
+
type,
|
|
77
|
+
privateKeyBase58: _multibaseMultikeyToBase58(key.secretKeyMultibase),
|
|
78
|
+
publicKeyBase58: _multibaseMultikeyToBase58(key.publicKeyMultibase)
|
|
79
|
+
};
|
|
80
|
+
keyDescription = {
|
|
81
|
+
'@context': ED25519_2018_V1_URL,
|
|
82
|
+
id: key.id,
|
|
83
|
+
type,
|
|
84
|
+
publicKeyBase58: key.publicKeyBase58,
|
|
85
|
+
controller
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// consistently order key description properties
|
|
90
|
+
{
|
|
91
|
+
const {
|
|
92
|
+
['@context']: context, id, type, ...rest
|
|
93
|
+
} = keyDescription;
|
|
94
|
+
keyDescription = {
|
|
95
|
+
'@context': context, id, type, ...rest, controller
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {key, keyDescription};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Signs some data. Note that the data will be sent to the server, so if
|
|
104
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
105
|
+
* hashing the data first may present interoperability issues so choose
|
|
106
|
+
* wisely.
|
|
107
|
+
*
|
|
108
|
+
* @ignore
|
|
109
|
+
* @param {object} options - The options to use.
|
|
110
|
+
* @param {object} options.key - The key to use.
|
|
111
|
+
* @param {object} options.operation - The KMS operation.
|
|
112
|
+
*
|
|
113
|
+
* @returns {Promise<object>} Contains `{signatureValue}`.
|
|
114
|
+
*/
|
|
115
|
+
export async function sign({key, operation}) {
|
|
116
|
+
// prepare `key` for import
|
|
117
|
+
const {type} = key;
|
|
118
|
+
if(type.startsWith('urn:webkms:multikey:')) {
|
|
119
|
+
// import key as a `Multikey`
|
|
120
|
+
key = {...key, type: 'Multikey'};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let keyPair;
|
|
124
|
+
if(type.startsWith('urn:webkms:multikey:P-')) {
|
|
125
|
+
keyPair = await EcdsaMultikey.from(key);
|
|
126
|
+
} else if(type.startsWith('urn:webkms:multikey:BBS-') ||
|
|
127
|
+
type === 'urn:webkms:multikey:Bls12381G2') {
|
|
128
|
+
keyPair = await Bls12381Multikey.from(key);
|
|
129
|
+
} else {
|
|
130
|
+
keyPair = await Ed25519Multikey.from(key);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const {sign} = keyPair.signer();
|
|
134
|
+
const {verifyData} = operation;
|
|
135
|
+
const signatureBytes = await sign({
|
|
136
|
+
data: Buffer.from(verifyData, 'base64url')
|
|
137
|
+
});
|
|
138
|
+
return {signatureValue: Buffer.from(signatureBytes).toString('base64url')};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _multibaseMultikeyToBase58(mb) {
|
|
142
|
+
// special transform for Ed25519 secret or public multibase multikey values
|
|
143
|
+
const mk = base58.decode(mb.slice(1));
|
|
144
|
+
return base58.encode(mk.slice(2));
|
|
145
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
|
|
6
|
+
const SUPPORTED_KEY_TYPES = new Map([
|
|
7
|
+
['Sha256HmacKey2019', 'https://w3id.org/security/suites/hmac-2019/v1']
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates a new HMAC key.
|
|
12
|
+
*
|
|
13
|
+
* @ignore
|
|
14
|
+
* @param {object} options - The options to use.
|
|
15
|
+
* @param {string} options.keyId - The key ID to use.
|
|
16
|
+
* @param {string} options.type - A KEY_TYPE.
|
|
17
|
+
* @param {string} options.controller - The key controller.
|
|
18
|
+
*
|
|
19
|
+
* @returns {Promise<object>} An object containing `{key, keyDescription}`.
|
|
20
|
+
*/
|
|
21
|
+
export async function generateKey({keyId, type, controller} = {}) {
|
|
22
|
+
const keyContextUrl = SUPPORTED_KEY_TYPES.get(type);
|
|
23
|
+
if(!keyContextUrl) {
|
|
24
|
+
throw new Error(`Unknown key type "${type}".`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const key = {
|
|
28
|
+
'@context': keyContextUrl,
|
|
29
|
+
id: keyId,
|
|
30
|
+
type,
|
|
31
|
+
secret: Buffer.from(crypto.randomBytes(32)).toString('base64url')
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const keyDescription = {
|
|
35
|
+
'@context': keyContextUrl,
|
|
36
|
+
id: keyId,
|
|
37
|
+
type,
|
|
38
|
+
controller
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {key, keyDescription};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Signs some data. Note that the data will be sent to the server, so if
|
|
46
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
47
|
+
* hashing the data first may present interoperability issues so choose
|
|
48
|
+
* wisely.
|
|
49
|
+
*
|
|
50
|
+
* @ignore
|
|
51
|
+
* @param {object} options - The options to use.
|
|
52
|
+
* @param {object} options.key - The key to use.
|
|
53
|
+
* @param {object} options.operation - The KMS operation.
|
|
54
|
+
*
|
|
55
|
+
* @returns {Promise<object>} Contains `{signatureValue}`.
|
|
56
|
+
*/
|
|
57
|
+
export async function sign({key, operation}) {
|
|
58
|
+
if(key.type !== 'Sha256HmacKey2019') {
|
|
59
|
+
throw new Error(`Unknown key type "${key.type}".`);
|
|
60
|
+
}
|
|
61
|
+
const {verifyData} = operation;
|
|
62
|
+
const signatureValue = await _hs256Sign({key, verifyData});
|
|
63
|
+
return {signatureValue: Buffer.from(signatureValue).toString('base64url')};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Verifies some data. Note that the data will be sent to the server, so if
|
|
68
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
69
|
+
* hashing the data first may present interoperability issues so choose
|
|
70
|
+
* wisely.
|
|
71
|
+
*
|
|
72
|
+
* @ignore
|
|
73
|
+
* @param {object} options - The options to use.
|
|
74
|
+
* @param {object} options.key - The key to use.
|
|
75
|
+
* @param {object} options.operation - The KMS operation.
|
|
76
|
+
*
|
|
77
|
+
* @returns {Promise<object>} An object containing `{verified}`.
|
|
78
|
+
*/
|
|
79
|
+
export async function verify({key, operation}) {
|
|
80
|
+
if(key.type !== 'Sha256HmacKey2019') {
|
|
81
|
+
throw new Error(`Unknown key type "${key.type}".`);
|
|
82
|
+
}
|
|
83
|
+
const {signatureValue, verifyData} = operation;
|
|
84
|
+
const verified = await _hs256Verify({key, verifyData, signatureValue});
|
|
85
|
+
return {verified};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function _hs256Sign({key, verifyData}) {
|
|
89
|
+
const secret = Buffer.from(key.secret, 'base64url');
|
|
90
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
91
|
+
hmac.update(Buffer.from(verifyData, 'base64url'));
|
|
92
|
+
return hmac.digest();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function _hs256Verify({key, verifyData, signatureValue}) {
|
|
96
|
+
signatureValue = Buffer.from(signatureValue, 'base64url');
|
|
97
|
+
const signatureCheck = await _hs256Sign({key, verifyData});
|
|
98
|
+
return crypto.timingSafeEqual(signatureValue, signatureCheck);
|
|
99
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import * as base58 from 'base58-universal';
|
|
5
|
+
import * as bedrock from '@bedrock/core';
|
|
6
|
+
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';
|
|
7
|
+
import {
|
|
8
|
+
X25519KeyAgreementKey2020
|
|
9
|
+
} from '@digitalbazaar/x25519-key-agreement-key-2020';
|
|
10
|
+
|
|
11
|
+
const {util: {BedrockError}} = bedrock;
|
|
12
|
+
|
|
13
|
+
const MULTIKEY_CONTEXT_V1_URL = 'https://w3id.org/security/multikey/v1';
|
|
14
|
+
|
|
15
|
+
const SUPPORTED_MULTIKEY_TYPES = new Map([
|
|
16
|
+
['ec01', 'urn:webkms:multikey:X25519'],
|
|
17
|
+
['8024', 'urn:webkms:multikey:ECDH-P-256'],
|
|
18
|
+
['8124', 'urn:webkms:multikey:ECDH-P-384'],
|
|
19
|
+
['8224', 'urn:webkms:multikey:ECDH-P-521']
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generates a new key agreement key pair.
|
|
24
|
+
*
|
|
25
|
+
* @ignore
|
|
26
|
+
* @param {object} options - The options to use.
|
|
27
|
+
* @param {string} options.keyId - The key ID to use.
|
|
28
|
+
* @param {object} options.type - The key type.
|
|
29
|
+
* @param {string} options.controller - The key controller.
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<object>} An object containing `{key, keyDescription}`.
|
|
32
|
+
*/
|
|
33
|
+
export async function generateKey({keyId, type, controller} = {}) {
|
|
34
|
+
let keyPair;
|
|
35
|
+
if(type.startsWith('urn:webkms:multikey:ECDH-P-')) {
|
|
36
|
+
const curve = type.slice('urn:webkms:multikey:ECDH-'.length);
|
|
37
|
+
keyPair = await EcdsaMultikey.generate({
|
|
38
|
+
id: keyId, curve, keyAgreement: true
|
|
39
|
+
});
|
|
40
|
+
} else if(type.includes('X25519')) {
|
|
41
|
+
keyPair = await X25519KeyAgreementKey2020.generate({id: keyId});
|
|
42
|
+
} else {
|
|
43
|
+
throw new BedrockError(`Unsupported key type "${type}".`, {
|
|
44
|
+
name: 'NotSupportedError',
|
|
45
|
+
details: {public: true, httpStatusCode: 400}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// generate full key portion for internal record
|
|
50
|
+
const key = await keyPair.export(
|
|
51
|
+
{publicKey: true, privateKey: true, secretKey: true, includeContext: true});
|
|
52
|
+
key.type = type;
|
|
53
|
+
|
|
54
|
+
// generate public key description
|
|
55
|
+
let keyDescription = await keyPair.export({
|
|
56
|
+
publicKey: true, includeContext: true
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// special handling for `X25519` Multikey
|
|
60
|
+
if(type == 'urn:webkms:multikey:X25519') {
|
|
61
|
+
key['@context'] = MULTIKEY_CONTEXT_V1_URL;
|
|
62
|
+
key.secretKeyMultibase = key.privateKeyMultibase;
|
|
63
|
+
delete key.privateKeyMultibase;
|
|
64
|
+
keyDescription['@context'] = MULTIKEY_CONTEXT_V1_URL;
|
|
65
|
+
keyDescription.type = 'Multikey';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// consistently order key description properties
|
|
69
|
+
{
|
|
70
|
+
const {
|
|
71
|
+
['@context']: context, id, type, ...rest
|
|
72
|
+
} = keyDescription;
|
|
73
|
+
keyDescription = {
|
|
74
|
+
'@context': context, id, type, ...rest, controller
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {key, keyDescription};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Signs some data. Note that the data will be sent to the server, so if
|
|
83
|
+
* this data is intended to be secret it should be hashed first. However,
|
|
84
|
+
* hashing the data first may present interoperability issues so choose
|
|
85
|
+
* wisely.
|
|
86
|
+
*
|
|
87
|
+
* @ignore
|
|
88
|
+
* @param {object} options - The options to use.
|
|
89
|
+
* @param {object} options.key - Exported key pair record, loaded from storage.
|
|
90
|
+
* @param {object} options.operation - The KMS operation.
|
|
91
|
+
*
|
|
92
|
+
* @returns {Promise<{secret: string}>} Resolves with the derived secret.
|
|
93
|
+
*/
|
|
94
|
+
export async function deriveSecret({key, operation}) {
|
|
95
|
+
const {publicKey} = operation;
|
|
96
|
+
const type = publicKey.type === 'Multikey' ?
|
|
97
|
+
_parseMultikey(publicKey.publicKeyMultibase) : publicKey.type;
|
|
98
|
+
if(type !== key.type) {
|
|
99
|
+
throw Error(
|
|
100
|
+
`The given public key type "${type}" does not match the ` +
|
|
101
|
+
`key agreement key's type "${key.type}".`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let keyPair;
|
|
105
|
+
if(key.type.includes('X25519')) {
|
|
106
|
+
// special handling for `X25519` Multikey
|
|
107
|
+
if(type === 'urn:webkms:multikey:X25519') {
|
|
108
|
+
key = {
|
|
109
|
+
...key,
|
|
110
|
+
privateKeyMultibase: key.secretKeyMultibase
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
keyPair = await X25519KeyAgreementKey2020.from(key);
|
|
114
|
+
} else {
|
|
115
|
+
// import `key` as a `Multikey`
|
|
116
|
+
keyPair = await EcdsaMultikey.from(
|
|
117
|
+
{...key, type: 'Multikey'}, {keyAgreement: true});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const secret = await keyPair.deriveSecret({publicKey});
|
|
121
|
+
return {secret: Buffer.from(secret).toString('base64url')};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _parseMultikey(mb) {
|
|
125
|
+
let multikey;
|
|
126
|
+
const mbHeader = mb?.[0];
|
|
127
|
+
if(mbHeader === 'z') {
|
|
128
|
+
multikey = base58.decode(mb.slice(1));
|
|
129
|
+
} else if(mbHeader === 'u') {
|
|
130
|
+
multikey = Buffer.from(mb.slice(1), 'base64url');
|
|
131
|
+
} else {
|
|
132
|
+
throw new BedrockError(`Unsupported multibase header "${mbHeader}".`, {
|
|
133
|
+
name: 'NotSupportedError',
|
|
134
|
+
details: {public: true, httpStatusCode: 400}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const header = Buffer.from(multikey.subarray(0, 2)).toString('hex');
|
|
139
|
+
const type = SUPPORTED_MULTIKEY_TYPES.get(header);
|
|
140
|
+
if(!type) {
|
|
141
|
+
throw new BedrockError(`Unsupported multikey header "${header}".`, {
|
|
142
|
+
name: 'NotSupportedError',
|
|
143
|
+
details: {public: true, httpStatusCode: 400}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return type;
|
|
148
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import * as _asymmetricKey from './asymmetricKey.js';
|
|
5
|
+
import * as _hmacKey from './hmacKey.js';
|
|
6
|
+
import * as _keyAgreementKey from './keyAgreementKey.js';
|
|
7
|
+
import * as _wrapKey from './wrapKey.js';
|
|
8
|
+
|
|
9
|
+
const OPERATIONS = new Map([
|
|
10
|
+
// asymmetric keys
|
|
11
|
+
['Ed25519VerificationKey2018', _asymmetricKey],
|
|
12
|
+
['Ed25519VerificationKey2020', _asymmetricKey],
|
|
13
|
+
['urn:webkms:multikey:Ed25519', _asymmetricKey],
|
|
14
|
+
['urn:webkms:multikey:P-256', _asymmetricKey],
|
|
15
|
+
['urn:webkms:multikey:P-384', _asymmetricKey],
|
|
16
|
+
['urn:webkms:multikey:P-521', _asymmetricKey],
|
|
17
|
+
['urn:webkms:multikey:BBS-BLS12-381-SHA-256', _asymmetricKey],
|
|
18
|
+
['urn:webkms:multikey:BBS-BLS12-381-SHAKE-256', _asymmetricKey],
|
|
19
|
+
['urn:webkms:multikey:Bls12381G2', _asymmetricKey],
|
|
20
|
+
// key agreement keys
|
|
21
|
+
['X25519KeyAgreementKey2020', _keyAgreementKey],
|
|
22
|
+
['urn:webkms:multikey:X25519', _keyAgreementKey],
|
|
23
|
+
['urn:webkms:multikey:ECDH-P-256', _keyAgreementKey],
|
|
24
|
+
['urn:webkms:multikey:ECDH-P-384', _keyAgreementKey],
|
|
25
|
+
['urn:webkms:multikey:ECDH-P-521', _keyAgreementKey],
|
|
26
|
+
// wrapping keys
|
|
27
|
+
['AesKeyWrappingKey2019', _wrapKey],
|
|
28
|
+
// hmac keys
|
|
29
|
+
['Sha256HmacKey2019', _hmacKey],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
export function getKeyOp({name, type}) {
|
|
33
|
+
const ops = OPERATIONS.get(type);
|
|
34
|
+
if(!ops) {
|
|
35
|
+
throw new Error(`Unknown key type "${type}".`);
|
|
36
|
+
}
|
|
37
|
+
const op = ops[name];
|
|
38
|
+
if(!op) {
|
|
39
|
+
throw new Error(`Unsupported operation "${name}" for key type "${type}".`);
|
|
40
|
+
}
|
|
41
|
+
return op;
|
|
42
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import {unwrapKey as aesUnwrapKey, wrapKey as aesWrapKey} from './aeskw.js';
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_KEY_TYPES = new Map([
|
|
8
|
+
['AesKeyWrappingKey2019', 'https://w3id.org/security/suites/aes-2019/v1']
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generates a new wraping key.
|
|
13
|
+
*
|
|
14
|
+
* @ignore
|
|
15
|
+
* @param {object} options - The options to use.
|
|
16
|
+
* @param {string} options.keyId - The key ID to use.
|
|
17
|
+
* @param {string} options.type - A KEY_TYPE.
|
|
18
|
+
* @param {string} options.controller - The key controller.
|
|
19
|
+
*
|
|
20
|
+
* @returns {Promise<object>} An object containing `{key, keyDescription}`.
|
|
21
|
+
*/
|
|
22
|
+
export async function generateKey({keyId, type, controller} = {}) {
|
|
23
|
+
const keyContextUrl = SUPPORTED_KEY_TYPES.get(type);
|
|
24
|
+
if(!keyContextUrl) {
|
|
25
|
+
throw new Error(`Unknown key type "${type}".`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const key = {
|
|
29
|
+
'@context': keyContextUrl,
|
|
30
|
+
id: keyId,
|
|
31
|
+
type,
|
|
32
|
+
secret: Buffer.from(crypto.randomBytes(32)).toString('base64url')
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const keyDescription = {
|
|
36
|
+
'@context': keyContextUrl,
|
|
37
|
+
id: keyId,
|
|
38
|
+
type,
|
|
39
|
+
controller
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {key, keyDescription};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wraps a cryptographic key using a key encryption key (KEK).
|
|
47
|
+
*
|
|
48
|
+
* @ignore
|
|
49
|
+
* @param {object} options - The options to use.
|
|
50
|
+
* @param {string} options.kek - The key encryption key to use.
|
|
51
|
+
* @param {object} options.operation - The KMS operation.
|
|
52
|
+
*
|
|
53
|
+
* @returns {Promise<object>} An object containing `{wrappedKey}`.
|
|
54
|
+
*/
|
|
55
|
+
export async function wrapKey({kek, operation}) {
|
|
56
|
+
const {unwrappedKey} = operation;
|
|
57
|
+
const wrappedKey = await _aesWrapKey({kek, unwrappedKey});
|
|
58
|
+
return {wrappedKey};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Unwraps a cryptographic key using a key encryption key (KEK).
|
|
63
|
+
*
|
|
64
|
+
* @ignore
|
|
65
|
+
* @param {object} options - The options to use.
|
|
66
|
+
* @param {string} options.kek - The key encryption key to use.
|
|
67
|
+
* @param {object} options.operation - The KMS operation.
|
|
68
|
+
*
|
|
69
|
+
* @returns {Promise<object>} An object containing `{unwrappedKey}`.
|
|
70
|
+
*/
|
|
71
|
+
export async function unwrapKey({kek, operation}) {
|
|
72
|
+
const {wrappedKey} = operation;
|
|
73
|
+
const unwrappedKey = await _aesUnwrapKey({kek, wrappedKey});
|
|
74
|
+
return {unwrappedKey};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function _aesWrapKey({kek, unwrappedKey}) {
|
|
78
|
+
const unwrapped = Buffer.from(unwrappedKey, 'base64url');
|
|
79
|
+
const secretKey = Buffer.from(kek.secret, 'base64url');
|
|
80
|
+
const output = await aesWrapKey({secretKey, unwrapped});
|
|
81
|
+
return Buffer.from(output).toString('base64url');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function _aesUnwrapKey({kek, wrappedKey}) {
|
|
85
|
+
const wrapped = Buffer.from(wrappedKey, 'base64url');
|
|
86
|
+
const secretKey = Buffer.from(kek.secret, 'base64url');
|
|
87
|
+
const output = await aesUnwrapKey({secretKey, wrapped});
|
|
88
|
+
return Buffer.from(output).toString('base64url');
|
|
89
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2019-2026 Digital Bazaar, Inc.
|
|
3
|
+
*/
|
|
4
|
+
import {KmsModule} from './KmsModule.js';
|
|
5
|
+
|
|
6
|
+
export {Core} from './core/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generates a KMS module interface that uses the given `Core` interface,
|
|
10
|
+
* `core`, and `KeyStorage` interface, `keyStorage`.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} options - The options to use.
|
|
13
|
+
* @param {Core} options.core - The `Core` interface to use.
|
|
14
|
+
* @param {KeyStorage} options.keyStorage - The `KeyStorage` interface to use.
|
|
15
|
+
*
|
|
16
|
+
* @returns {Promise<object>} An object with `{kmsModule, api}`.
|
|
17
|
+
*/
|
|
18
|
+
export function createKmsModule({core, keyStorage} = {}) {
|
|
19
|
+
const kmsModule = new KmsModule({core, keyStorage});
|
|
20
|
+
const descriptors = Object.getOwnPropertyDescriptors(KmsModule.prototype);
|
|
21
|
+
const apiEntries = [];
|
|
22
|
+
for(const [name, descriptor] of Object.entries(descriptors)) {
|
|
23
|
+
if(typeof descriptor.value !== 'function' || name === 'constructor') {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
apiEntries.push([name, descriptor.value.bind(kmsModule)]);
|
|
27
|
+
}
|
|
28
|
+
const api = Object.fromEntries(apiEntries);
|
|
29
|
+
return {kmsModule, api};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* An interface with WebKMS core functions.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {object} Core
|
|
36
|
+
* @property {Function} generateKey
|
|
37
|
+
* @property {Function} getKeyDescription
|
|
38
|
+
* @property {Function} wrapKey
|
|
39
|
+
* @property {Function} unwrapKey
|
|
40
|
+
* @property {Function} sign
|
|
41
|
+
* @property {Function} verify
|
|
42
|
+
* @property {Function} deriveSecret
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* An interface for storing WebKMS keys.
|
|
47
|
+
*
|
|
48
|
+
* @typedef {object} KeyStorage
|
|
49
|
+
* @property {Function} insert
|
|
50
|
+
* @property {Function} getCount
|
|
51
|
+
* @property {Function} get
|
|
52
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bedrock/kms-module-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Bedrock WebKMS module core",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.md",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib/**/*.js"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"lint": "eslint ."
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/digitalbazaar/bedrock-kms-module-core"
|
|
17
|
+
},
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Digital Bazaar, Inc.",
|
|
20
|
+
"email": "support@digitalbazaar.com",
|
|
21
|
+
"url": "https://digitalbazaar.com"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/digitalbazaar/bedrock-kms-module-core/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/digitalbazaar/bedrock-kms-module-core",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@digitalbazaar/bls12-381-multikey": "^2.1.0",
|
|
29
|
+
"@digitalbazaar/ecdsa-multikey": "^1.8.0",
|
|
30
|
+
"@digitalbazaar/ed25519-multikey": "^1.3.1",
|
|
31
|
+
"@digitalbazaar/x25519-key-agreement-key-2020": "^3.0.1",
|
|
32
|
+
"assert-plus": "^1.0.0",
|
|
33
|
+
"base58-universal": "^2.0.0",
|
|
34
|
+
"url-template": "^3.1.1"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@bedrock/core": "^6.3.0",
|
|
38
|
+
"@bedrock/kms-module-key-storage": "^1.2.0",
|
|
39
|
+
"@bedrock/mongodb": "^11.0.1",
|
|
40
|
+
"@bedrock/record-cipher": "^1.2.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"eslint": "^8.57.1",
|
|
47
|
+
"eslint-config-digitalbazaar": "^5.2.0",
|
|
48
|
+
"eslint-plugin-jsdoc": "^50.6.3",
|
|
49
|
+
"eslint-plugin-unicorn": "^56.0.1",
|
|
50
|
+
"jsdoc": "^4.0.4",
|
|
51
|
+
"jsdoc-to-markdown": "^9.1.1"
|
|
52
|
+
}
|
|
53
|
+
}
|