@venn-lang/crypto 0.1.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 +21 -0
- package/README.md +134 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +444 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/actions/encoding-actions.ts +34 -0
- package/src/actions/hash-actions.ts +70 -0
- package/src/actions/index.ts +13 -0
- package/src/actions/jwt-actions.ts +77 -0
- package/src/actions/password-actions.ts +66 -0
- package/src/bytes/bytes.ts +70 -0
- package/src/bytes/index.ts +12 -0
- package/src/engines/fake-engine.ts +43 -0
- package/src/engines/index.ts +2 -0
- package/src/engines/web-crypto-engine.ts +45 -0
- package/src/index.ts +20 -0
- package/src/jwt/decode.ts +37 -0
- package/src/jwt/index.ts +2 -0
- package/src/jwt/jwt.types.ts +9 -0
- package/src/plugin.ts +18 -0
- package/src/port/crypto-engine.port.ts +15 -0
- package/src/port/crypto-engine.types.ts +23 -0
- package/src/port/index.ts +2 -0
- package/src/types.ts +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vinicius Borges
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @venn-lang/crypto
|
|
2
|
+
|
|
3
|
+
> The `crypto` namespace: digests, HMACs, base64, PBKDF2 password hashing and JSON Web Tokens.
|
|
4
|
+
|
|
5
|
+
Auth flows need to mint a token, check a signature, or prove that a stored password is not the
|
|
6
|
+
password. This package gives thirteen verbs for that. All of them run over the `CryptoEngine`
|
|
7
|
+
port, whose real implementation is the platform's WebCrypto, so the package stays
|
|
8
|
+
`platform: neutral` and needs no native module.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
The package is part of the stdlib the `venn` CLI loads, so nothing to install. Reach it from a
|
|
13
|
+
flow with a `use` line:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
use "venn/crypto"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The plugin requires no host capability: cryptography here is computation, not I/O.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
module demo.session
|
|
25
|
+
|
|
26
|
+
use "venn/assert"
|
|
27
|
+
use "venn/crypto"
|
|
28
|
+
|
|
29
|
+
flow "A session token round-trips" {
|
|
30
|
+
const secret = "s3cret"
|
|
31
|
+
|
|
32
|
+
step "the password is stored hashed, never in the clear" {
|
|
33
|
+
const stored = crypto.password.hash "correct horse" { iterations: 1000 }
|
|
34
|
+
expect stored contains "pbkdf2$sha256$1000$"
|
|
35
|
+
|
|
36
|
+
let ok = crypto.password.verify "correct horse" { hash: stored }
|
|
37
|
+
expect ok == true
|
|
38
|
+
|
|
39
|
+
let wrong = crypto.password.verify "wrong horse" { hash: stored }
|
|
40
|
+
expect wrong == false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
step "the token verifies, and its claims read back" {
|
|
44
|
+
const token = crypto.jwt.sign { payload: { sub: "alice" }, secret: secret }
|
|
45
|
+
|
|
46
|
+
let ok = crypto.jwt.verify token { secret: secret }
|
|
47
|
+
expect ok == true
|
|
48
|
+
|
|
49
|
+
const decoded = crypto.jwt.decode token
|
|
50
|
+
expect decoded.header.alg == "HS256"
|
|
51
|
+
expect decoded.payload.sub == "alice"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Verbs
|
|
57
|
+
|
|
58
|
+
| Verb | Options | Gives back |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| `crypto.hash data` | `algorithm` (`sha1`, `sha256`, `sha384`, `sha512`; default `sha256`) | The digest, lowercase hex. |
|
|
61
|
+
| `crypto.hmac data` | `key` (required), `algorithm` | The keyed digest, lowercase hex. |
|
|
62
|
+
| `crypto.randomBytes` | `size` (default 16) | Random bytes, hex-encoded. |
|
|
63
|
+
| `crypto.uuid` | none | A random v4 UUID. |
|
|
64
|
+
| `crypto.base64.encode text` | none | Base64. |
|
|
65
|
+
| `crypto.base64.decode text` | none | The original string. |
|
|
66
|
+
| `crypto.base64url.encode text` | none | Base64url, the flavour JWT uses. |
|
|
67
|
+
| `crypto.base64url.decode text` | none | The original string. |
|
|
68
|
+
| `crypto.jwt.sign` | `payload` (required), `secret` (required), `algorithm` (`HS256`, `HS384`, `HS512`; default `HS256`) | The signed token. |
|
|
69
|
+
| `crypto.jwt.verify token` | `secret` (required) | `true` when the signature matches. |
|
|
70
|
+
| `crypto.jwt.decode token` | none | `crypto.Jwt`: `header`, `payload`, `signature`, `signingInput`. |
|
|
71
|
+
| `crypto.password.hash password` | `iterations` (default 100000), `algorithm` (`sha256` or `sha512`) | The encoded hash. |
|
|
72
|
+
| `crypto.password.verify password` | `hash` (required) | `true` when the password produced that hash. |
|
|
73
|
+
|
|
74
|
+
`crypto.jwt.sign` reads everything from options, so nothing goes in argument position. The rest
|
|
75
|
+
take their subject positionally.
|
|
76
|
+
|
|
77
|
+
### Passwords
|
|
78
|
+
|
|
79
|
+
`crypto.password.hash` uses PBKDF2, not bcrypt: WebCrypto offers PBKDF2, which keeps this package
|
|
80
|
+
free of a native dependency. The result is self-describing, so verifying needs nothing but the
|
|
81
|
+
string itself:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
pbkdf2$sha256$100000$<salt>$<derived>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A fresh 16-byte salt is drawn per call, so hashing the same password twice gives two different
|
|
88
|
+
strings and both verify. Comparison is constant-time.
|
|
89
|
+
|
|
90
|
+
### Tokens
|
|
91
|
+
|
|
92
|
+
`crypto.jwt.decode` splits and decodes a token **without verifying it**. Reading a token's claims
|
|
93
|
+
and trusting them are two different acts, and `crypto.jwt.verify` is the second one. A token that
|
|
94
|
+
is not base64url-encoded JSON raises `VN7003`. Verification recomputes the HMAC over the token's
|
|
95
|
+
own `signingInput` and compares in constant time, so a payload edited after signing fails.
|
|
96
|
+
|
|
97
|
+
## The CryptoEngine port
|
|
98
|
+
|
|
99
|
+
`venn.port.crypto-engine`, contract version 1, requires no capability, four methods: `digest`,
|
|
100
|
+
`hmac`, `derive`, `randomBytes`. Everything returns lowercase hex, which is the one shape the
|
|
101
|
+
verbs convert from.
|
|
102
|
+
|
|
103
|
+
| Implementation | Behaviour |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| `createWebCryptoEngine()` | The real one, backed by `crypto.subtle`. Available in Node 24 and in browsers. |
|
|
106
|
+
| `createFakeCryptoEngine()` | Deterministic FNV-1a stand-in. Same input, same output, never secure; it exists so a flow's assertions replay. |
|
|
107
|
+
|
|
108
|
+
Both run the same conformance suite. The stdlib binds the **real** engine by default, even though
|
|
109
|
+
it binds fakes for everything else: hashing is pure computation, not a side effect, so there is
|
|
110
|
+
nothing to isolate a test from.
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
| Export | What it is |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `cryptoPlugin` | The `PluginDefinition` for the `crypto` namespace. |
|
|
117
|
+
| `CryptoEnginePort` | The port descriptor. |
|
|
118
|
+
| `CryptoEngine`, `DeriveArgs`, `HashAlgorithm` | The interface an engine satisfies, its `derive` arguments, and the four digests. |
|
|
119
|
+
| `createWebCryptoEngine`, `createFakeCryptoEngine` | The two implementations. |
|
|
120
|
+
| `decodeJwt(token)`, `DecodedJwt` | The splitter behind `crypto.jwt.decode`, usable directly. |
|
|
121
|
+
| `toBytes`, `fromBytes` | String to `Uint8Array` and back, UTF-8. |
|
|
122
|
+
| `toHex`, `fromHex` | Bytes to lowercase hex and back. |
|
|
123
|
+
| `toBase64`, `fromBase64` | Bytes to base64 and back. |
|
|
124
|
+
| `toBase64Url`, `fromBase64Url` | The same, with `+/` as `-_` and no padding. |
|
|
125
|
+
| `equals(left, right)` | Constant-time string comparison, so a verification cannot be timed. |
|
|
126
|
+
|
|
127
|
+
The plugin publishes one named type, `crypto.Jwt`. Every other verb answers with a string or a
|
|
128
|
+
boolean, which the signature says inline.
|
|
129
|
+
|
|
130
|
+
## See also
|
|
131
|
+
|
|
132
|
+
- [`@venn-lang/data`](../std-data) for the passwords and identities to hash.
|
|
133
|
+
- [`@venn-lang/auth`](../std-auth) for building the headers a token goes into.
|
|
134
|
+
- [`@venn-lang/contracts`](../contracts) for `Port`, `Host` and capability negotiation.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Port } from "@venn-lang/contracts";
|
|
2
|
+
import { PluginDefinition } from "@venn-lang/sdk";
|
|
3
|
+
//#region src/bytes/bytes.d.ts
|
|
4
|
+
/** Bytes backed by a plain `ArrayBuffer`, the only shape WebCrypto accepts. */
|
|
5
|
+
type Bytes = Uint8Array<ArrayBuffer>;
|
|
6
|
+
/** UTF-8 encode a string. */
|
|
7
|
+
declare function toBytes(text: string): Bytes;
|
|
8
|
+
/** UTF-8 decode bytes back to a string. Invalid sequences become the replacement character. */
|
|
9
|
+
declare function fromBytes(bytes: Uint8Array): string;
|
|
10
|
+
/** Lowercase hex, two characters per byte. */
|
|
11
|
+
declare function toHex(bytes: Uint8Array): string;
|
|
12
|
+
/** Read a hex string back into bytes. A trailing odd character is dropped. */
|
|
13
|
+
declare function fromHex(hex: string): Bytes;
|
|
14
|
+
/** Standard base64, padded. */
|
|
15
|
+
declare function toBase64(bytes: Uint8Array): string;
|
|
16
|
+
/**
|
|
17
|
+
* Read standard base64 back into bytes.
|
|
18
|
+
*
|
|
19
|
+
* @throws DOMException when the text is not valid base64.
|
|
20
|
+
*/
|
|
21
|
+
declare function fromBase64(text: string): Bytes;
|
|
22
|
+
/** Base64url, the flavour JWT uses: `+/` become `-_` and padding is dropped. */
|
|
23
|
+
declare function toBase64Url(bytes: Uint8Array): string;
|
|
24
|
+
/**
|
|
25
|
+
* Read base64url back into bytes, restoring the padding first.
|
|
26
|
+
*
|
|
27
|
+
* @throws DOMException when the text is not valid base64url.
|
|
28
|
+
*/
|
|
29
|
+
declare function fromBase64Url(text: string): Bytes;
|
|
30
|
+
/**
|
|
31
|
+
* Compare two strings in time that does not depend on where they differ.
|
|
32
|
+
*
|
|
33
|
+
* Use this for every signature and digest check: `===` returns early on the
|
|
34
|
+
* first mismatched byte, which leaks the correct prefix to a patient attacker.
|
|
35
|
+
*/
|
|
36
|
+
declare function equals(left: string, right: string): boolean;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/port/crypto-engine.types.d.ts
|
|
39
|
+
/** The digests every engine supports. */
|
|
40
|
+
type HashAlgorithm = "sha1" | "sha256" | "sha384" | "sha512";
|
|
41
|
+
/** Arguments for {@link CryptoEngine.derive}: a password plus its PBKDF2 cost parameters. */
|
|
42
|
+
interface DeriveArgs {
|
|
43
|
+
password: string;
|
|
44
|
+
salt: string;
|
|
45
|
+
iterations: number;
|
|
46
|
+
algorithm: HashAlgorithm;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The primitives every `crypto` verb is built from.
|
|
50
|
+
*
|
|
51
|
+
* Each method answers in lowercase hex. That single shape is what the actions
|
|
52
|
+
* convert from, so an engine that returned bytes would break every caller.
|
|
53
|
+
*/
|
|
54
|
+
interface CryptoEngine {
|
|
55
|
+
digest(args: {
|
|
56
|
+
algorithm: HashAlgorithm;
|
|
57
|
+
data: string;
|
|
58
|
+
}): Promise<string>;
|
|
59
|
+
hmac(args: {
|
|
60
|
+
algorithm: HashAlgorithm;
|
|
61
|
+
key: string;
|
|
62
|
+
data: string;
|
|
63
|
+
}): Promise<string>;
|
|
64
|
+
derive(args: DeriveArgs): Promise<string>;
|
|
65
|
+
randomBytes(size: number): string;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/port/crypto-engine.port.d.ts
|
|
69
|
+
/**
|
|
70
|
+
* The port every `crypto` verb reaches its primitives through.
|
|
71
|
+
*
|
|
72
|
+
* Bound by the host to `createWebCryptoEngine` or `createFakeCryptoEngine`.
|
|
73
|
+
* Requires no capability, because WebCrypto is present on every target.
|
|
74
|
+
*/
|
|
75
|
+
declare const CryptoEnginePort: Port<CryptoEngine>;
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/engines/fake-engine.d.ts
|
|
78
|
+
/**
|
|
79
|
+
* A deterministic stand-in for tests: same input, same output, no platform crypto.
|
|
80
|
+
*
|
|
81
|
+
* Not secure and not meant to be. It exists so assertions over a digest, an HMAC
|
|
82
|
+
* or a random token stay reproducible from run to run.
|
|
83
|
+
*/
|
|
84
|
+
declare function createFakeCryptoEngine(): CryptoEngine;
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/engines/web-crypto-engine.d.ts
|
|
87
|
+
/**
|
|
88
|
+
* The real engine, backed by the platform's global WebCrypto.
|
|
89
|
+
*
|
|
90
|
+
* WebCrypto exists in Node 24 and in browsers alike, so nothing here imports
|
|
91
|
+
* `node:crypto` and the package stays platform-neutral.
|
|
92
|
+
*/
|
|
93
|
+
declare function createWebCryptoEngine(): CryptoEngine;
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/jwt/jwt.types.d.ts
|
|
96
|
+
/** A JWT taken apart, verified or not. */
|
|
97
|
+
interface DecodedJwt {
|
|
98
|
+
header: Record<string, unknown>;
|
|
99
|
+
payload: Record<string, unknown>;
|
|
100
|
+
/** The third section, still base64url. Empty when the token carries none. */
|
|
101
|
+
signature: string;
|
|
102
|
+
/** `header.payload`, exactly the bytes the signature covers. */
|
|
103
|
+
signingInput: string;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/jwt/decode.d.ts
|
|
107
|
+
/**
|
|
108
|
+
* Split a token and decode its header and payload, without verifying anything.
|
|
109
|
+
*
|
|
110
|
+
* Reading a token's claims and trusting them are separate acts. This is the
|
|
111
|
+
* first; `crypto.jwt.verify` is the second.
|
|
112
|
+
*
|
|
113
|
+
* @param token A compact JWS, `header.payload.signature`.
|
|
114
|
+
* @returns The decoded sections plus the `signingInput` a signature covers.
|
|
115
|
+
* @throws VennError `VN7003` when the token has fewer than two sections, or when
|
|
116
|
+
* a section is not base64url-encoded JSON.
|
|
117
|
+
*/
|
|
118
|
+
declare function decodeJwt(token: string): DecodedJwt;
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/plugin.d.ts
|
|
121
|
+
/**
|
|
122
|
+
* The `crypto` plugin: digests, encodings, password hashing and JSON Web Tokens.
|
|
123
|
+
*
|
|
124
|
+
* Every primitive comes from `CryptoEnginePort`, so a host can swap WebCrypto for
|
|
125
|
+
* the deterministic fake and keep assertions reproducible. No capability is
|
|
126
|
+
* required: WebCrypto is present in Node and in the browser alike.
|
|
127
|
+
*/
|
|
128
|
+
declare const cryptoPlugin: PluginDefinition;
|
|
129
|
+
//#endregion
|
|
130
|
+
export { type CryptoEngine, CryptoEnginePort, type DecodedJwt, type DeriveArgs, type HashAlgorithm, createFakeCryptoEngine, createWebCryptoEngine, cryptoPlugin, decodeJwt, equals, fromBase64, fromBase64Url, fromBytes, fromHex, toBase64, toBase64Url, toBytes, toHex };
|
|
131
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/bytes/bytes.ts","../src/port/crypto-engine.types.ts","../src/port/crypto-engine.port.ts","../src/engines/fake-engine.ts","../src/engines/web-crypto-engine.ts","../src/jwt/jwt.types.ts","../src/jwt/decode.ts","../src/plugin.ts"],"mappings":";;;;KAIY,QAAQ,WAAW;;iBAGf,QAAQ,eAAe;;iBAKvB,UAAU,OAAO;;iBAKjB,MAAM,OAAO;;iBAKb,QAAQ,cAAc;;iBAMtB,SAAS,OAAO;;;;;;iBAShB,WAAW,eAAe;;iBAK1B,YAAY,OAAO;;;;;;iBASnB,cAAc,eAAe;;;;;;;iBAW7B,OAAO,cAAc;;;;KC7DzB;;UAGK;EACf;EACA;EACA;EACA,WAAW;;;;;;;;UASI;EACf,OAAO;IAAQ,WAAW;IAAe;MAAiB;EAC1D,KAAK;IAAQ,WAAW;IAAe;IAAa;MAAiB;EACrE,OAAO,MAAM,aAAa;EAC1B,YAAY;;;;;;;;;;cCZD,kBAAkB,KAAK;;;;;;;;;iBCDpB,0BAA0B;;;;;;;;;iBCQ1B,yBAAyB;;;;UCfxB;EACf,QAAQ;EACR,SAAS;;EAET;;EAEA;;;;;;;;;;;;;;;iBCQc,UAAU,gBAAgB;;;;;;;;;;cCJ7B,cAAc"}
|