Skip to content

Commit 16e92af

Browse files
authored
implement ES256/384/512
implement ES256/384/512
2 parents 63edfd8 + 11d319a commit 16e92af

File tree

11 files changed

+530
-29
lines changed

11 files changed

+530
-29
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@solid/keychain",
3-
"version": "0.3.5",
3+
"version": "0.4.0",
44
"description": "KeyChain for use with Web Cryptography API in Node.js",
55
"main": "src/index.js",
66
"directories": {
@@ -38,7 +38,7 @@
3838
},
3939
"homepage": "https://github.com/solid/keychain#README",
4040
"dependencies": {
41-
"@solid/jose": "^0.6.9",
41+
"@solid/jose": "^0.7.0",
4242
"base64url": "^3.0.1"
4343
},
4444
"devDependencies": {

src/KeyChain.js

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Dependencies
33
*/
44
const supportedAlgorithms = require('./algorithms')
5+
const InvalidDescriptorError = require('./errors/InvalidDescriptorError')
56

67
/**
78
* KeyChain
@@ -10,14 +11,21 @@ class KeyChain {
1011

1112
/**
1213
* constructor
14+
*
15+
* @param data {Object} Keychain data
16+
* @param options {Object} Optional configuration
17+
* @param options.crypto {Object} Optional crypto instance for cross-package compatibility
1318
*/
14-
constructor (data) {
19+
constructor (data, options = {}) {
1520
// use data as the descriptor if descriptor property is missing
1621
if (!data.descriptor) {
1722
data = { descriptor: data }
1823
}
1924

2025
Object.assign(this, data)
26+
27+
// Store crypto instance if provided
28+
this._crypto = options.crypto
2129
}
2230

2331
/**
@@ -34,14 +42,14 @@ class KeyChain {
3442
* @param {Object} params
3543
* @return {Promise}
3644
*/
37-
static generateKey (params) {
45+
static generateKey (params, crypto) {
3846
let normalizedAlgorithm = supportedAlgorithms.normalize('generateKey', params.alg)
3947

4048
if (normalizedAlgorithm instanceof Error) {
4149
return Promise.reject(normalizedAlgorithm)
4250
}
4351

44-
let algorithm = new normalizedAlgorithm(params)
52+
let algorithm = new normalizedAlgorithm({...params, crypto})
4553

4654
return algorithm.generateKey()
4755
}
@@ -53,24 +61,28 @@ class KeyChain {
5361
* @param {Object} jwk
5462
* @return {Promise}
5563
*/
56-
static importKey (jwk) {
64+
static importKey (jwk, crypto) {
5765
let {alg} = jwk
5866
let normalizedAlgorithm = supportedAlgorithms.normalize('importKey', alg)
5967

6068
if (normalizedAlgorithm instanceof Error) {
6169
return Promise.reject(normalizedAlgorithm)
6270
}
6371

64-
let algorithm = new normalizedAlgorithm({alg})
72+
let algorithm = new normalizedAlgorithm({alg, crypto})
6573

6674
return algorithm.importKey(jwk)
6775
}
6876

6977
/**
7078
* restore
79+
*
80+
* @param data {Object} Keychain data
81+
* @param options {Object} Optional configuration
82+
* @param options.crypto {Object} Optional crypto instance for cross-package compatibility
7183
*/
72-
static restore (data) {
73-
let keys = new KeyChain(data)
84+
static restore (data, options) {
85+
let keys = new KeyChain(data, options)
7486
return keys.importKeys().then(() => keys)
7587
}
7688

@@ -84,22 +96,44 @@ class KeyChain {
8496

8597
// import key
8698
if (props.includes('alg')) {
87-
return KeyChain.importKey(object).then(cryptoKey => {
88-
if (cryptoKey.type === 'private') {
99+
return KeyChain.importKey(object, this._crypto).then(cryptoKey => {
100+
if (cryptoKey.type === 'private' && !container.privateKey) {
89101
Object.defineProperty(container, 'privateKey', {
90102
enumerable: false,
91103
value: cryptoKey
92104
})
93105
}
94106

95-
if (cryptoKey.type === 'public') {
107+
if (cryptoKey.type === 'public' && !container.publicKey) {
96108
Object.defineProperty(container, 'publicKey', {
97109
enumerable: false,
98110
value: cryptoKey
99111
})
100112
}
101113
})
102114

115+
// import key pair structure (has privateJwk and publicJwk)
116+
} else if (props.includes('privateJwk') && props.includes('publicJwk')) {
117+
// Import both private and public keys
118+
return Promise.all([
119+
KeyChain.importKey(object.privateJwk, this._crypto),
120+
KeyChain.importKey(object.publicJwk, this._crypto)
121+
]).then(([privateKey, publicKey]) => {
122+
if (!object.privateKey) {
123+
Object.defineProperty(object, 'privateKey', {
124+
enumerable: false,
125+
value: privateKey
126+
})
127+
}
128+
129+
if (!object.publicKey) {
130+
Object.defineProperty(object, 'publicKey', {
131+
enumerable: false,
132+
value: publicKey
133+
})
134+
}
135+
})
136+
103137
// recurse
104138
} else {
105139
return Promise.all(
@@ -109,7 +143,7 @@ class KeyChain {
109143
let subProps = Object.keys(subObject)
110144
//console.log('RECURSE WITH', name, subDescriptor, subObject, subProps)
111145

112-
this.importKeys({
146+
return this.importKeys({
113147
descriptor: subDescriptor,
114148
object: subObject,
115149
container: object,
@@ -141,7 +175,7 @@ class KeyChain {
141175
// generate key(pair), assign resulting object to keychain,
142176
// and add JWK for public key to JWK Set
143177
if (params.alg) {
144-
return KeyChain.generateKey(params).then(result => {
178+
return KeyChain.generateKey(params, this._crypto).then(result => {
145179
container[key] = result
146180

147181
if (result.publicJwk) {
@@ -164,7 +198,7 @@ class KeyChain {
164198

165199
// invalid descriptor
166200
} else {
167-
throw new InvalidDescriptorError(key, value)
201+
throw new InvalidDescriptorError(key, params)
168202
}
169203
})
170204
).then(() => {

src/algorithms/EcKeyPair.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Dependencies
3+
*/
4+
const { crypto } = require('@solid/jose')
5+
const base64url = require('base64url')
6+
7+
/**
8+
* EcKeyPair
9+
*/
10+
class EcKeyPair {
11+
12+
/**
13+
* constructor
14+
*
15+
* @param params {Object} Options hashmap
16+
* @param params.alg {string} For example, 'ES256', 'ES384', 'ES512'
17+
* @param params.namedCurve {string} For example, 'P-256', 'P-384', 'P-521'
18+
* @param params.usages {Array<string>}
19+
* @param params.crypto {Object} Optional crypto instance to use (for cross-package compatibility)
20+
*/
21+
constructor (params) {
22+
let name = 'ECDSA'
23+
let {alg, namedCurve, usages} = params
24+
25+
// Allow overriding crypto instance for cross-package compatibility
26+
this.crypto = params.crypto || crypto
27+
28+
// Map algorithm to curve and hash
29+
let algorithmMap = {
30+
'ES256': { curve: 'P-256', hash: 'SHA-256' },
31+
'ES384': { curve: 'P-384', hash: 'SHA-384' },
32+
'ES512': { curve: 'P-521', hash: 'SHA-512' } // Note: P-521, not P-512
33+
}
34+
35+
let algConfig = algorithmMap[alg]
36+
37+
if (!algConfig) {
38+
throw new Error(`Unsupported EC algorithm: ${alg}`)
39+
}
40+
41+
// Use provided namedCurve or default from algorithm
42+
if (!namedCurve) {
43+
namedCurve = algConfig.curve
44+
}
45+
46+
let hash = { name: algConfig.hash }
47+
48+
if (!usages) {
49+
usages = ['sign', 'verify']
50+
}
51+
52+
this.alg = alg
53+
this.algorithm = {name, namedCurve, hash}
54+
this.extractable = true
55+
this.usages = usages
56+
}
57+
58+
/**
59+
* generateKey
60+
*/
61+
generateKey () {
62+
let {algorithm, extractable, usages} = this
63+
64+
return this.crypto.subtle
65+
.generateKey(algorithm, extractable, usages)
66+
.then(this.setCryptoKeyPair)
67+
.then(result => this.setJwkKeyPair(result))
68+
}
69+
70+
/**
71+
* importKey
72+
*/
73+
importKey (jwk) {
74+
let {name, namedCurve, hash} = this.algorithm
75+
let algorithm = {name, namedCurve, hash}
76+
let extractable = true
77+
let usages = jwk.key_ops
78+
79+
return this.crypto.subtle
80+
.importKey('jwk', jwk, algorithm, extractable, usages)
81+
}
82+
83+
/**
84+
* setCryptoKeyPair
85+
*/
86+
setCryptoKeyPair (cryptoKeyPair) {
87+
let result = {}
88+
89+
Object.defineProperty(result, 'privateKey', {
90+
enumerable: false,
91+
value: cryptoKeyPair.privateKey
92+
})
93+
94+
Object.defineProperty(result, 'publicKey', {
95+
enumerable: false,
96+
value: cryptoKeyPair.publicKey
97+
})
98+
99+
return result
100+
}
101+
102+
/**
103+
* setJwkKeyPair
104+
*/
105+
setJwkKeyPair (result) {
106+
return Promise.all([
107+
this.crypto.subtle.exportKey('jwk', result.privateKey),
108+
this.crypto.subtle.exportKey('jwk', result.publicKey)
109+
])
110+
.then(jwks => {
111+
let [privateJwk, publicJwk] = jwks
112+
113+
result.privateJwk = Object.assign({
114+
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8)))),
115+
alg: this.alg
116+
}, privateJwk)
117+
118+
result.publicJwk = Object.assign({
119+
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8)))),
120+
alg: this.alg
121+
}, publicJwk)
122+
123+
return result
124+
})
125+
}
126+
}
127+
128+
/**
129+
* Export
130+
*/
131+
module.exports = EcKeyPair

src/algorithms/RsaKeyPair.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@ class RsaKeyPair {
1717
* @param params.modulusLength {number}
1818
* @param params.publicExponent {BufferSource} For example, a Uint8Array
1919
* @param params.usages {Array<string>}
20+
* @param params.crypto {Object} Optional crypto instance to use (for cross-package compatibility)
2021
*/
2122
constructor (params) {
2223
let name = 'RSASSA-PKCS1-v1_5'
2324
let {alg, modulusLength, publicExponent, usages} = params
2425
let hashLengthValid = alg.match(/(256|384|512)$/)
2526
let hashLength = hashLengthValid && hashLengthValid.shift()
2627
let hash = { name: `SHA-${hashLength}` }
28+
29+
// Allow overriding crypto instance for cross-package compatibility
30+
this.crypto = params.crypto || crypto
2731

2832
if (!hashLength) {
2933
throw new Error('Invalid hash length')
@@ -52,10 +56,10 @@ class RsaKeyPair {
5256
generateKey () {
5357
let {algorithm, extractable, usages} = this
5458

55-
return crypto.subtle
59+
return this.crypto.subtle
5660
.generateKey(algorithm, extractable, usages)
5761
.then(this.setCryptoKeyPair)
58-
.then(this.setJwkKeyPair)
62+
.then((result) => this.setJwkKeyPair(result))
5963
}
6064

6165
/**
@@ -67,7 +71,7 @@ class RsaKeyPair {
6771
let extractable = true
6872
let usages = jwk.key_ops
6973

70-
return crypto.subtle
74+
return this.crypto.subtle
7175
.importKey('jwk', jwk, algorithm, extractable, usages)
7276
}
7377

@@ -95,18 +99,18 @@ class RsaKeyPair {
9599
*/
96100
setJwkKeyPair (result) {
97101
return Promise.all([
98-
crypto.subtle.exportKey('jwk', result.privateKey),
99-
crypto.subtle.exportKey('jwk', result.publicKey)
102+
this.crypto.subtle.exportKey('jwk', result.privateKey),
103+
this.crypto.subtle.exportKey('jwk', result.publicKey)
100104
])
101105
.then(jwks => {
102106
let [privateJwk, publicJwk] = jwks
103107

104108
result.privateJwk = Object.assign({
105-
kid: base64url(Buffer.from(crypto.getRandomValues(new Uint8Array(8))))
109+
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8))))
106110
}, privateJwk)
107111

108112
result.publicJwk = Object.assign({
109-
kid: base64url(Buffer.from(crypto.getRandomValues(new Uint8Array(8))))
113+
kid: base64url(Buffer.from(this.crypto.getRandomValues(new Uint8Array(8))))
110114
}, publicJwk)
111115

112116
return result

0 commit comments

Comments
 (0)