Skip to content

Commit f9a2127

Browse files
Initial work on node:crypto bindings
0 parents  commit f9a2127

19 files changed

+1193
-0
lines changed

.gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/bower_components/
2+
/node_modules/
3+
/.pulp-cache/
4+
/output/
5+
/generated-docs/
6+
/.psc-package/
7+
/.psc*
8+
/.purs*
9+
/.psa*
10+
/.spago
11+
12+
/.vscode

packages.dhall

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
let upstream =
2+
https://github.com/purescript/package-sets/releases/download/psc-0.15.14-20240123/packages.dhall
3+
sha256:bb64d773919b5992d37a50050c4bc0e4358bda2f0eabfa494e7d3444ca683556
4+
5+
in upstream

spago.dhall

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{ name = "node-crypto"
2+
, dependencies =
3+
[ "effect"
4+
, "either"
5+
, "exceptions"
6+
, "maybe"
7+
, "node-buffer"
8+
, "node-streams"
9+
, "nullable"
10+
, "prelude"
11+
, "unsafe-coerce"
12+
]
13+
, packages = ./packages.dhall
14+
, sources = [ "src/**/*.purs" ]
15+
}

src/Node/Crypto/Certificate.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Certificate } from "node:crypto";
2+
3+
export const exportChallengeImpl = (spkac) => Certificate.exportChallenge(spkac);
4+
export const exportChallengeEncodingImpl = (spkac, encoding) => Certificate.exportChallenge(spkac, encoding);
5+
6+
export const exportPublicKeyImpl = (spkac) => Certificate.exportPublicKey(spkac);
7+
export const exportPublicKeyEncodingImpl = (spkac, encoding) => Certificate.exportPublicKey(spkac, encoding);
8+
9+
export const verifySpkacImpl = (spkac) => Certificate.verifySpkac(spkac);
10+
export const verifySpkacEncodingImpl = (spkac, encoding) => Certificate.verifySpkac(spkac, encoding);

src/Node/Crypto/Certificate.purs

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
-- | SPKAC is a Certificate Signing Request mechanism originally implemented by Netscape and was specified formally as part of HTML5's `keygen` element.
2+
-- |
3+
-- | `<keygen>` is deprecated since [HTML 5.2](https://www.w3.org/TR/html52/changes.html#features-removed) and new projects should not use this element anymore.
4+
-- |
5+
-- | The node:crypto module provides the Certificate class for working with SPKAC data.
6+
-- | The most common usage is handling output generated by the HTML5 <keygen> element.
7+
-- | Node.js uses [OpenSSL's SPKAC implementation](https://www.openssl.org/docs/man3.0/man1/openssl-spkac.html) internally.
8+
module Node.Crypto.Certificate where
9+
10+
import Effect (Effect)
11+
import Effect.Uncurried (EffectFn1, EffectFn2, runEffectFn1, runEffectFn2)
12+
import Node.Buffer (Buffer)
13+
import Node.Encoding (Encoding, encodingToNode)
14+
15+
foreign import exportChallengeImpl :: EffectFn1 (Buffer) (Buffer)
16+
17+
-- | Returns the challenge component of the spkac data structure, which includes a public key and a challenge.
18+
exportChallenge :: Buffer -> Effect Buffer
19+
exportChallenge buffer = runEffectFn1 exportChallengeImpl buffer
20+
21+
foreign import exportChallengeEncodingImpl :: EffectFn2 (String) (String) (Buffer)
22+
23+
-- | Returns the challenge component of the spkac data structure, which includes a public key and a challenge.
24+
exportChallenge' :: String -> Encoding -> Effect Buffer
25+
exportChallenge' str encoding =
26+
runEffectFn2 exportChallengeEncodingImpl str (encodingToNode encoding)
27+
28+
foreign import exportPublicKeyImpl :: EffectFn1 (Buffer) (Buffer)
29+
30+
-- | Returns the public key component of the spkac data structure, which includes a public key and a challenge.
31+
exportPublicKey :: Buffer -> Effect Buffer
32+
exportPublicKey buffer = runEffectFn1 exportPublicKeyImpl buffer
33+
34+
foreign import exportPublicKeyEncodingImpl :: EffectFn2 (String) (String) (Buffer)
35+
36+
-- | Returns the public key component of the spkac data structure, which includes a public key and a challenge.
37+
exportPublicKey' :: String -> Encoding -> Effect Buffer
38+
exportPublicKey' str encoding =
39+
runEffectFn2 exportPublicKeyEncodingImpl str (encodingToNode encoding)
40+
41+
foreign import verifySpkacImpl :: EffectFn1 (Buffer) (Boolean)
42+
43+
-- | Returns `true` if the given spkac data structure is valid, `false` otherwise.
44+
verifySpkac :: Buffer -> Effect Boolean
45+
verifySpkac buffer = runEffectFn1 verifySpkacImpl buffer
46+
47+
foreign import verifySpkacEncodingImpl :: EffectFn2 (String) (String) (Boolean)
48+
49+
-- | Returns `true` if the given spkac data structure is valid, `false` otherwise.
50+
verifySpkac' :: String -> Encoding -> Effect Boolean
51+
verifySpkac' str encoding =
52+
runEffectFn2 verifySpkacEncodingImpl str (encodingToNode encoding)
53+
54+
55+

src/Node/Crypto/Cipher.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as crypto from "node:crypto";
2+
export const newImpl = (algorithm, key, iv) => crypto.createCipheriv(algorithm, key, iv);
3+
export const newOptsImpl = (algorithm, key, iv, options) => crypto.createCipheriv(algorithm, key, iv, options);
4+
export const finalBufImpl = (cipher) => cipher.final();
5+
export const finalStrImpl = (cipher, encoding) => cipher.final(encoding);
6+
export const getAuthTagImpl = (cipher) => cipher.getAuthTag();
7+
export const setAADImpl = (cipher, buffer) => cipher.setAAD(buffer);
8+
export const setAADOptsImpl = (cipher, buffer, opts) => cipher.setAAD(buffer, opts);
9+
export const setAutoPaddingImpl = (cipher) => cipher.setAutoPadding();
10+
export const setAutoPaddingBoolImpl = (cipher, autoPadding) => cipher.setAutoPadding(autoPadding);
11+
export const updateBufBufImpl = (cipher, buf) => cipher.update(buf);
12+
export const updateBufStrImpl = (cipher, buf, outEncoding) => cipher.update(buf, outEncoding);
13+
export const updateStrBufImpl = (cipher, str, inputEncoding) => cipher.update(str, inputEncoding);
14+
export const updateStrStrImpl = (cipher, str, inputEncoding, outputEncoding) => cipher.update(str, inputEncoding, outputEncoding);

src/Node/Crypto/Cipher.purs

+220
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
module Node.Crypto.Cipher
2+
( Cipher
3+
, new
4+
, NewOptions
5+
, new'
6+
, toDuplex
7+
, finalBuf
8+
, finalStr
9+
, getAuthTag
10+
, setAAD
11+
, SetAADOptions
12+
, setAAD'
13+
, setAutoPadding
14+
, setAutoPadding'
15+
, updateBufBuf
16+
, updateBufStr
17+
, updateStrBuf
18+
, updateStrStr
19+
) where
20+
21+
22+
import Data.Nullable (Nullable)
23+
import Effect (Effect)
24+
import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4)
25+
import Node.Buffer (Buffer)
26+
import Node.Encoding (Encoding, encodingToNode)
27+
import Node.Stream (Duplex)
28+
import Prim.Row as Row
29+
import Unsafe.Coerce (unsafeCoerce)
30+
31+
-- | Instances of the `Cipher` class are used to encrypt data. The class can be used in one of two ways:
32+
-- | - As a stream that is both readable and writable, where plain unencrypted data is written to produce encrypted data on the readable side, or
33+
-- | - Using the `cipher.update()` and `cipher.final()` methods to produce the encrypted data.
34+
foreign import data Cipher :: Type
35+
36+
foreign import newImpl :: EffectFn3 (String) (Buffer) (Nullable Buffer) (Cipher)
37+
38+
-- | Creates and returns a Cipher object, with the given `algorithm`, `key`, and `initialization vector` (IV).
39+
-- |
40+
-- | Use `new'` instead of this function when using
41+
-- | - A cipher in CCM or OCB mode as the `authTagLength` options is required and specifies the length of the authentication tag in bytes
42+
-- | - A cipher in GCM mode and you want to use the `authTagLength` option to set the length of the authentication tag that is returned by `getAuthTag` (defaults to 16 bytes).
43+
-- |
44+
-- | For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes.
45+
-- |
46+
-- | The `algorithm` is dependent on OpenSSL, examples are 'aes192', etc. On recent OpenSSL releases,
47+
-- | `openssl list -cipher-algorithms` will display the available cipher algorithms.
48+
-- |
49+
-- | If the cipher does not need an IV, it may be null.
50+
-- |
51+
-- | Initialization vectors should be unpredictable and unique; ideally, they will be cryptographically random.
52+
-- | They do not have to be secret: IVs are typically just added to ciphertext messages unencrypted.
53+
-- | It may sound contradictory that something has to be unpredictable and unique, but does not have to be secret;
54+
-- | remember that an attacker must not be able to predict ahead of time what a given IV will be.
55+
new :: String -> Buffer -> Nullable Buffer -> Effect Cipher
56+
new algorithm key initializationBuffer =
57+
runEffectFn3 newImpl algorithm key initializationBuffer
58+
59+
foreign import newOptsImpl :: forall r. EffectFn4 (String) (Buffer) (Nullable Buffer) ({ | r }) (Cipher)
60+
61+
type NewOptions =
62+
( authTagLength :: Number
63+
)
64+
65+
-- | Creates and returns a Cipher object, with the given `algorithm`, `key`, `initialization vector` (IV), and `options`.
66+
-- |
67+
-- | The `options` argument controls stream behavior and is optional except when a cipher in CCM or OCB mode (e.g. 'aes-128-ccm') is used.
68+
-- | In that case, the `authTagLength` option is required and specifies the length of the authentication tag in bytes, see CCM mode.
69+
-- | In GCM mode, the `authTagLength` option is not required but can be used to set the length of the authentication tag that
70+
-- | will be returned by `getAuthTag()` and defaults to 16 bytes.
71+
-- | For `chacha20-poly1305`, the `authTagLength` option defaults to 16 bytes.
72+
-- |
73+
-- | The `algorithm` is dependent on OpenSSL, examples are 'aes192', etc. On recent OpenSSL releases,
74+
-- | `openssl list -cipher-algorithms` will display the available cipher algorithms.
75+
-- |
76+
-- | If the cipher does not need an initialization vector, it may be null.
77+
-- |
78+
-- | Initialization vectors should be unpredictable and unique; ideally, they will be cryptographically random.
79+
-- | They do not have to be secret: IVs are typically just added to ciphertext messages unencrypted.
80+
-- | It may sound contradictory that something has to be unpredictable and unique, but does not have to be secret;
81+
-- | remember that an attacker must not be able to predict ahead of time what a given IV will be.
82+
new' :: forall r trash. Row.Union r trash NewOptions => String -> Buffer -> Nullable Buffer -> { | r } -> Effect Cipher
83+
new' algorithm key initializationBuffer options =
84+
runEffectFn4 newOptsImpl algorithm key initializationBuffer options
85+
86+
toDuplex :: Cipher -> Duplex
87+
toDuplex = unsafeCoerce
88+
89+
foreign import finalBufImpl :: EffectFn1 (Cipher) (Buffer)
90+
91+
-- | Once the `cipher.final()` method has been called,
92+
-- | the `Cipher` object can no longer be used to encrypt data.
93+
-- | Attempts to call `cipher.final()` more than once will result in an error being thrown.
94+
finalBuf :: Cipher -> Effect Buffer
95+
finalBuf cipher = runEffectFn1 finalBufImpl cipher
96+
97+
foreign import finalStrImpl :: EffectFn2 (Cipher) (String) (Buffer)
98+
99+
-- | Once the `cipher.final()` method has been called,
100+
-- | the `Cipher` object can no longer be used to encrypt data.
101+
-- | Attempts to call `cipher.final()` more than once will result in an error being thrown.
102+
-- |
103+
-- | `cipher # finalStr UTF8`
104+
finalStr :: Encoding -> Cipher -> Effect Buffer
105+
finalStr encoding cipher = runEffectFn2 finalStrImpl cipher (encodingToNode encoding)
106+
107+
foreign import getAuthTagImpl :: EffectFn1 (Cipher) (Buffer)
108+
109+
110+
-- | When using an authenticated encryption mode (GCM, CCM, OCB, and chacha20-poly1305 are currently supported),
111+
-- | the `cipher.getAuthTag()` method returns a `Buffer` containing the authentication tag that has been computed from the given data.
112+
-- |
113+
-- | The `cipher.getAuthTag()` method should only be called after encryption has been completed using the `cipher.final()` method.
114+
-- |
115+
-- | If the `authTagLength` option was set during the `cipher` instance's creation, this function will return exactly `authTagLength` bytes.
116+
getAuthTag :: Cipher -> Effect Buffer
117+
getAuthTag cipher = runEffectFn1 getAuthTagImpl cipher
118+
119+
foreign import setAADImpl :: EffectFn2 (Cipher) (Buffer) (Cipher)
120+
121+
-- | When using an authenticated encryption mode (GCM, CCM, OCB, and chacha20-poly1305 are currently supported),
122+
-- | the `cipher.setAAD()` method sets the value used for the _additional authenticated data_ (AAD) input parameter.
123+
-- |
124+
-- | The `cipher.setAAD()` method must be called before `cipher.update()`.
125+
setAAD :: Buffer -> Cipher -> Effect Cipher
126+
setAAD buffer cipher =
127+
runEffectFn2 setAADImpl cipher buffer
128+
129+
type SetAADOptions =
130+
( plainTextLength :: Number
131+
, encoding :: String
132+
)
133+
134+
foreign import setAADOptsImpl :: forall r. EffectFn3 (Cipher) (Buffer) ({ | r }) (Cipher)
135+
136+
-- | When using an authenticated encryption mode (GCM, CCM, OCB, and chacha20-poly1305 are currently supported),
137+
-- | the `cipher.setAAD()` method sets the value used for the additional authenticated data (AAD) input parameter.
138+
-- |
139+
-- | The `plaintextLength` option is optional for GCM and OCB. When using CCM, the `plaintextLength` option must
140+
-- | be specified and its value must match the length of the plaintext in bytes. See CCM mode.
141+
-- |
142+
-- | The `cipher.setAAD()` method must be called before `cipher.update()`.
143+
setAAD'
144+
:: forall r trash
145+
. Row.Union r trash SetAADOptions
146+
=> Buffer
147+
-> { | r }
148+
-> Cipher
149+
-> Effect Cipher
150+
setAAD' buffer r cipher =
151+
runEffectFn3 setAADOptsImpl cipher buffer r
152+
153+
foreign import setAutoPaddingImpl :: EffectFn1 (Cipher) (Cipher)
154+
155+
-- | Sets the `cipher`'s `autoPadding` to `true`.
156+
-- |
157+
-- | When using block encryption algorithms, the `Cipher` class will automatically
158+
-- | add padding to the input data to the appropriate block size. To disable the default padding call `cipher.setAutoPadding(false)`.
159+
-- |
160+
-- | When `autoPadding` is `false`, the length of the entire input data must be a
161+
-- | multiple of the cipher's block size or `cipher.final()` will throw an error.
162+
-- | Disabling automatic padding is useful for non-standard padding, for instance using `0x0` instead of PKCS padding.
163+
-- |
164+
-- | The `cipher.setAutoPadding()` method must be called before `cipher.final()`.
165+
setAutoPadding :: Cipher -> Effect Cipher
166+
setAutoPadding cipher = runEffectFn1 setAutoPaddingImpl cipher
167+
168+
foreign import setAutoPaddingBoolImpl :: EffectFn2 (Cipher) (Boolean) (Cipher)
169+
170+
-- | When using block encryption algorithms, the `Cipher` class will automatically
171+
-- | add padding to the input data to the appropriate block size. To disable the default padding call `cipher.setAutoPadding(false)`.
172+
-- |
173+
-- | When `autoPadding` is `false`, the length of the entire input data must be a
174+
-- | multiple of the cipher's block size or `cipher.final()` will throw an error.
175+
-- | Disabling automatic padding is useful for non-standard padding, for instance using `0x0` instead of PKCS padding.
176+
-- |
177+
-- | The `cipher.setAutoPadding()` method must be called before `cipher.final()`.
178+
setAutoPadding' :: Boolean -> Cipher -> Effect Cipher
179+
setAutoPadding' boolean cipher =
180+
runEffectFn2 setAutoPaddingBoolImpl cipher boolean
181+
182+
foreign import updateBufBufImpl :: EffectFn2 (Cipher) (Buffer) (Buffer)
183+
184+
-- | Updates the `cipher`. This variant's input is `Buffer` and output is `Buffer`.
185+
-- |
186+
-- | The `cipher.update()` method can be called multiple times with new data until `cipher.final()` is called. Calling `cipher.update()` after `cipher.final()` will result in an error being thrown.
187+
updateBufBuf :: Buffer -> Cipher -> Effect Buffer
188+
updateBufBuf buffer cipher =
189+
runEffectFn2 updateBufBufImpl cipher buffer
190+
191+
foreign import updateBufStrImpl :: EffectFn3 (Cipher) (Buffer) (String) (String)
192+
193+
-- | Updates the `cipher`. This variant's input is `Buffer` and output is `String` using the speified encoding.
194+
-- |
195+
-- | The `cipher.update()` method can be called multiple times with new data until `cipher.final()` is called. Calling `cipher.update()` after `cipher.final()` will result in an error being thrown.
196+
updateBufStr :: Buffer -> Encoding -> Cipher -> Effect String
197+
updateBufStr buffer outputEncoding cipher =
198+
runEffectFn3 updateBufStrImpl cipher buffer (encodingToNode outputEncoding)
199+
200+
foreign import updateStrBufImpl :: EffectFn3 (Cipher) (String) (String) (Buffer)
201+
202+
-- | Updates the `cipher`. This variant's input is `String` using the specified encoding and output is `Buffer`.
203+
-- |
204+
-- | The `cipher.update()` method can be called multiple times with new data until `cipher.final()` is called. Calling `cipher.update()` after `cipher.final()` will result in an error being thrown.
205+
updateStrBuf :: String -> Encoding -> Cipher -> Effect Buffer
206+
updateStrBuf string inputEncoding cipher =
207+
runEffectFn3 updateStrBufImpl cipher string (encodingToNode inputEncoding)
208+
209+
foreign import updateStrStrImpl :: EffectFn4 (Cipher) (String) (String) (String) (String)
210+
211+
-- | Updates the `cipher`. This variant's input is `String` using the first specified encoding and output is `String` using the second specified encoding.
212+
-- |
213+
-- | The `cipher.update()` method can be called multiple times with new data until `cipher.final()` is called. Calling `cipher.update()` after `cipher.final()` will result in an error being thrown.
214+
-- | ```
215+
-- | updateStr cipher input inputEncoding outputEncoding
216+
-- | ```
217+
updateStrStr :: String -> Encoding -> Encoding -> Cipher -> Effect String
218+
updateStrStr string inputEncoding outputEncoding cipher =
219+
runEffectFn4 updateStrStrImpl cipher string (encodingToNode inputEncoding) (encodingToNode outputEncoding)
220+

src/Node/Crypto/Decipher.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import * as crypto from "node:crypto";
2+
export const newImpl = (algorithm, key, iv) => crypto.createDecipheriv(algorithm, key, iv);
3+
export const newOptsImpl = (algorithm, key, iv, options) => crypto.createDecipheriv(algorithm, key, iv, options);
4+
export const finalBufImpl = (decipher) => decipher.final();
5+
export const finalStrImpl = (decipher, encoding) => decipher.final(encoding);
6+
export const setAuthTagBufImpl = (decipher, buf) => decipher.getAuthTag(buf);
7+
export const setAuthTagStrImpl = (decipher, str, encoding) => decipher.getAuthTag(str, encoding);
8+
export const setAADImpl = (decipher, buffer) => decipher.setAAD(buffer);
9+
export const setAADOptsImpl = (decipher, buffer, opts) => decipher.setAAD(buffer, opts);
10+
export const setAutoPaddingImpl = (decipher) => decipher.setAutoPadding();
11+
export const setAutoPaddingBoolImpl = (decipher, autoPadding) => decipher.setAutoPadding(autoPadding);
12+
export const updateBufBufImpl = (decipher, buf) => decipher.update(buf);
13+
export const updateBufStrImpl = (decipher, buf, outEncoding) => decipher.update(buf, outEncoding);
14+
export const updateStrBufImpl = (decipher, str, inputEncoding) => decipher.update(str, inputEncoding);
15+
export const updateStrStrImpl = (decipher, str, inputEncoding, outputEncoding) => decipher.update(str, inputEncoding, outputEncoding);

0 commit comments

Comments
 (0)