forked from forcedotcom/lwc-dev-mobile-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add CryptoUtils to generate self-signed cert
- Loading branch information
1 parent
01850d6
commit 08fd9ae
Showing
6 changed files
with
278 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
network-timeout 600000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# error:invalidKeySize | ||
|
||
Invalid value for argument 'keySize'. It must be between 2048 and 16384. | ||
|
||
# error:invalidValidityDate | ||
|
||
Invalid value for argument 'validity'. It must be between 1 and 825 days in the future. | ||
|
||
# error:invalidValidityNumber | ||
|
||
Invalid value for argument 'validity'. It must be between 1 and 825. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
import forge from 'node-forge'; | ||
import { Messages } from '@salesforce/core'; | ||
|
||
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); | ||
|
||
const messages = Messages.loadMessages('@salesforce/lwc-dev-mobile-core', 'crypto-utils'); | ||
|
||
export type PEMCertificate = { | ||
certificate: string; | ||
key: string; | ||
}; | ||
|
||
export class CryptoUtils { | ||
/** | ||
* Generates a self-signed certificated using the provided parameters. | ||
* | ||
* @param {string} hostname the hostname to use. Defaults to LOCALHOST. | ||
* @param {number} keySize the size for the private key in bits, which must | ||
* be between 2048 and 16384. Defaults to 4096. | ||
* @param {number | Date} validity the validity length for the certificate. | ||
* It can either be a number (representing the number of days from today) or | ||
* it can be a specific date (which has to be at least 24 hours from now). | ||
* Defaults to 365 days. The maximum length cannot be more than 825 days in the | ||
* future to meet Apple guidelines: https://support.apple.com/en-gb/103769 | ||
* @returns the generated certificate and key. | ||
*/ | ||
public static generateSelfSignedCert( | ||
hostname: string = 'localhost', | ||
keySize: number = 4096, | ||
validity: number | Date = 365 | ||
): PEMCertificate { | ||
if (keySize < 2048 || keySize > 16_384) { | ||
throw new Error(messages.getMessage('error:invalidKeySize')); | ||
} | ||
|
||
if (validity instanceof Date) { | ||
const millisecondsInOneDay = 24 * 60 * 60 * 1000; | ||
const diff = validity.getTime() - new Date().getTime(); | ||
if (diff < millisecondsInOneDay || diff > 825 * millisecondsInOneDay) { | ||
throw new Error(messages.getMessage('error:invalidValidityDate')); | ||
} | ||
} else if (validity < 1 || validity > 825) { | ||
throw new Error(messages.getMessage('error:invalidValidityNumber')); | ||
} | ||
|
||
const keys = forge.pki.rsa.generateKeyPair(keySize); | ||
const cert = forge.pki.createCertificate(); | ||
|
||
const startDate = new Date(); | ||
let endDate = new Date(); | ||
if (validity instanceof Date) { | ||
endDate = validity; | ||
} else { | ||
endDate.setDate(startDate.getDate() + validity); | ||
} | ||
|
||
cert.publicKey = keys.publicKey; | ||
cert.serialNumber = '01'; | ||
cert.validity.notBefore = startDate; | ||
cert.validity.notAfter = endDate; | ||
|
||
const attrs = [ | ||
{ | ||
name: 'commonName', | ||
value: hostname | ||
}, | ||
{ | ||
name: 'countryName', | ||
value: 'US' | ||
}, | ||
{ | ||
shortName: 'ST', | ||
value: 'California' | ||
}, | ||
{ | ||
name: 'localityName', | ||
value: 'San Francisco' | ||
}, | ||
{ | ||
name: 'organizationName', | ||
value: 'Example Inc.' | ||
}, | ||
{ | ||
shortName: 'OU', | ||
value: 'Test' | ||
} | ||
]; | ||
|
||
cert.setSubject(attrs); | ||
cert.setIssuer(attrs); | ||
|
||
// Add the subjectAltName and ExtendedKeyUsage extensions, which are required by Apple | ||
cert.setExtensions([ | ||
{ | ||
name: 'basicConstraints', | ||
cA: true | ||
}, | ||
{ | ||
name: 'keyUsage', | ||
keyCertSign: true, | ||
digitalSignature: true, | ||
nonRepudiation: true, | ||
keyEncipherment: true, | ||
dataEncipherment: true | ||
}, | ||
{ | ||
name: 'extKeyUsage', | ||
serverAuth: true, | ||
clientAuth: true, | ||
codeSigning: true, | ||
emailProtection: true, | ||
timeStamping: true | ||
}, | ||
{ | ||
name: 'subjectAltName', | ||
altNames: [ | ||
{ | ||
type: 2, // DNS | ||
value: hostname | ||
}, | ||
{ | ||
type: 7, // IP | ||
value: '127.0.0.1' | ||
}, | ||
{ | ||
type: 7, // IP | ||
value: '10.0.2.2' | ||
}, | ||
{ | ||
type: 7, // IP (IPv6 loopback) | ||
value: '::1' | ||
} | ||
] | ||
} | ||
]); | ||
|
||
cert.sign(keys.privateKey, forge.md.sha256.create()); | ||
|
||
const pemCert = forge.pki.certificateToPem(cert); | ||
const pemKey = forge.pki.privateKeyToPem(keys.privateKey); | ||
|
||
return { certificate: pemCert, key: pemKey }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
/* | ||
* Copyright (c) 2024, salesforce.com, inc. | ||
* All rights reserved. | ||
* SPDX-License-Identifier: MIT | ||
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT | ||
*/ | ||
import { expect } from 'chai'; | ||
import { Messages } from '@salesforce/core'; | ||
import { CryptoUtils } from '../../../src/common/CryptoUtils.js'; | ||
|
||
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); | ||
|
||
describe('CryptoUtils tests', () => { | ||
const messages = Messages.loadMessages('@salesforce/lwc-dev-mobile-core', 'crypto-utils'); | ||
|
||
it('generateSelfSignedCert throws on invalid key size', async () => { | ||
const expectedErrorMsg = messages.getMessage('error:invalidKeySize'); | ||
|
||
try { | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 1024); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
|
||
try { | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 20000); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
}); | ||
|
||
it('generateSelfSignedCert throws on invalid validity as number of days', async () => { | ||
const expectedErrorMsg = messages.getMessage('error:invalidValidityNumber'); | ||
|
||
try { | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 2048, 0); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
|
||
try { | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 2048, 826); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
}); | ||
|
||
it('generateSelfSignedCert throws on invalid validity as specific date', async () => { | ||
const expectedErrorMsg = messages.getMessage('error:invalidValidityDate'); | ||
|
||
try { | ||
const expiryTooShort = new Date(); | ||
expiryTooShort.setHours(expiryTooShort.getHours() + 8); // less than 24 hours | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 2048, expiryTooShort); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
|
||
try { | ||
const expiryTooLong = new Date(); | ||
expiryTooLong.setDate(expiryTooLong.getDate() + 826); // more than 825 days in the future | ||
CryptoUtils.generateSelfSignedCert('MyHostName', 2048, expiryTooLong); | ||
} catch (error) { | ||
expect(error).to.be.an('error').with.property('message', expectedErrorMsg); | ||
} | ||
}); | ||
|
||
it('generateSelfSignedCert succeeds to generate a certificate and key for localhost', async () => { | ||
const cert = CryptoUtils.generateSelfSignedCert(); | ||
expect(cert.certificate.startsWith('-----BEGIN CERTIFICATE-----')).to.be.true; | ||
expect(cert.key.startsWith('-----BEGIN RSA PRIVATE KEY-----')).to.be.true; | ||
}).timeout(10000); // increase timeout for this test | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2166,6 +2166,13 @@ | |
dependencies: | ||
"@types/node" "*" | ||
|
||
"@types/node-forge@^1.3.11": | ||
version "1.3.11" | ||
resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" | ||
integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== | ||
dependencies: | ||
"@types/node" "*" | ||
|
||
"@types/node@*", "@types/node@^20.10.7", "@types/node@^20.12.11", "@types/node@^20.12.7": | ||
version "20.12.11" | ||
resolved "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz" | ||
|
@@ -5589,6 +5596,11 @@ node-fetch@^2.6.1, node-fetch@^2.6.9: | |
dependencies: | ||
whatwg-url "^5.0.0" | ||
|
||
node-forge@^1.3.1: | ||
version "1.3.1" | ||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" | ||
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== | ||
|
||
node-preload@^0.2.1: | ||
version "0.2.1" | ||
resolved "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz" | ||
|
@@ -6760,7 +6772,16 @@ srcset@^5.0.0: | |
resolved "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz" | ||
integrity sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw== | ||
|
||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | ||
"string-width-cjs@npm:string-width@^4.2.0": | ||
version "4.2.3" | ||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | ||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||
dependencies: | ||
emoji-regex "^8.0.0" | ||
is-fullwidth-code-point "^3.0.0" | ||
strip-ansi "^6.0.1" | ||
|
||
string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | ||
version "4.2.3" | ||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" | ||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||
|
@@ -6829,7 +6850,14 @@ string_decoder@~1.1.1: | |
dependencies: | ||
safe-buffer "~5.1.0" | ||
|
||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1: | ||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1": | ||
version "6.0.1" | ||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | ||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||
dependencies: | ||
ansi-regex "^5.0.1" | ||
|
||
[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1: | ||
version "6.0.1" | ||
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" | ||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||
|
@@ -7375,7 +7403,7 @@ [email protected]: | |
resolved "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz" | ||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== | ||
|
||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: | ||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": | ||
version "7.0.0" | ||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | ||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | ||
|
@@ -7393,6 +7421,15 @@ wrap-ansi@^6.2.0: | |
string-width "^4.1.0" | ||
strip-ansi "^6.0.0" | ||
|
||
wrap-ansi@^7.0.0: | ||
version "7.0.0" | ||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" | ||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== | ||
dependencies: | ||
ansi-styles "^4.0.0" | ||
string-width "^4.1.0" | ||
strip-ansi "^6.0.0" | ||
|
||
wrap-ansi@^8.1.0: | ||
version "8.1.0" | ||
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" | ||
|