git.iko.soy/lynxjs-elm/compiler
on master help sign in
478 lines 11 kB history raw
1# lynxjs-elm/crypto
3Elliptic curve cryptography and authenticated encryption for Elm on LynxJS.
5Embeds [@noble/curves](https://github.com/paulmillr/noble-curves) 2.0.1 and [@noble/ciphers](https://github.com/paulmillr/noble-ciphers) as kernel JS. All byte data is represented as hex-encoded strings.
7## Setup
9Add to your `elm.json`:
11```json
13 "dependencies": {
14 "direct": {
15 "lynxjs-elm/crypto": "1.0.0"
16 }
17 }
19```
21## Modules
23- [`Crypto.Secp256k1`](#cryptosecp256k1) — ECDSA signing, ECDH key exchange (Bitcoin, Ethereum)
24- [`Crypto.Ed25519`](#cryptoed25519) — EdDSA signing (SSH, Signal)
25- [`Crypto.X25519`](#cryptox25519) — ECDH key exchange (WireGuard, Signal, Noise)
26- [`Crypto.P256`](#cryptop256) — ECDSA signing, ECDH key exchange (TLS, WebAuthn)
27- [`Crypto.Cipher`](#cryptocipher) — AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305
29---
31## Crypto.Secp256k1
33```elm
34import Crypto.Secp256k1 as Secp256k1
35```
37### Types
39```elm
40type PrivateKey
41type PublicKey
42type Signature
43```
45All are opaque. Construct with `fromHex`, extract with `toHex`.
47### Key Conversion
49```elm
50privateKeyFromHex : String -> Maybe PrivateKey
51```
53Parse a hex-encoded private key (32 bytes / 64 hex chars). Returns `Nothing` if invalid or not a valid scalar on the curve.
55```elm
56privateKeyToHex : PrivateKey -> String
57```
59```elm
60publicKeyFromHex : String -> Maybe PublicKey
61```
63Parse a hex-encoded public key (compressed 33 bytes or uncompressed 65 bytes). Returns `Nothing` if the point is not on the curve.
65```elm
66publicKeyToHex : PublicKey -> String
67```
69Returns the compressed representation.
71### Signature Conversion
73```elm
74signatureFromCompactHex : String -> Maybe Signature
75```
77Parse a compact signature (r‖s, 64 bytes / 128 hex chars).
79```elm
80signatureFromDerHex : String -> Maybe Signature
81```
83Parse a DER-encoded signature.
85```elm
86signatureToCompactHex : Signature -> String
87signatureToDerHex : Signature -> String
88```
90### Operations
92```elm
93getPublicKey : PrivateKey -> PublicKey
94```
96Derive the compressed public key from a private key.
98```elm
99sign : String -> PrivateKey -> Signature
100```
102Sign a message hash. The first argument is the **hex-encoded hash** of the message (e.g. a SHA-256 hash). Uses deterministic RFC 6979 nonces.
104```elm
105verify : Signature -> String -> PublicKey -> Bool
106```
108Verify a signature against a message hash and public key.
110```elm
111getSharedSecret : PrivateKey -> PublicKey -> String
112```
114ECDH: compute a shared secret from your private key and their public key. Returns the hex-encoded shared point. Both parties derive the same secret.
116### Example
118```elm
119import Crypto.Secp256k1 as Secp256k1
121case Secp256k1.privateKeyFromHex "a665a459...7a27ae3" of
122 Just priv ->
123 let
124 pub = Secp256k1.getPublicKey priv
125 sig = Secp256k1.sign msgHashHex priv
126 in
127 Secp256k1.verify sig msgHashHex pub -- True
129 Nothing ->
130 False
131```
133---
135## Crypto.Ed25519
137```elm
138import Crypto.Ed25519 as Ed25519
139```
141### Types
143```elm
144type PrivateKey
145type PublicKey
146type Signature
147```
149### Key Conversion
151```elm
152privateKeyFromHex : String -> Maybe PrivateKey
153```
155Parse a hex-encoded private key (32 bytes / 64 hex chars).
157```elm
158privateKeyToHex : PrivateKey -> String
159```
161```elm
162publicKeyFromHex : String -> Maybe PublicKey
163```
165Parse a hex-encoded public key (32 bytes / 64 hex chars). Returns `Nothing` if the point is not on the curve.
167```elm
168publicKeyToHex : PublicKey -> String
169```
171### Signature Conversion
173```elm
174signatureFromHex : String -> Maybe Signature
175```
177Parse a hex-encoded signature (64 bytes / 128 hex chars).
179```elm
180signatureToHex : Signature -> String
181```
183### Operations
185```elm
186getPublicKey : PrivateKey -> PublicKey
187```
189```elm
190sign : String -> PrivateKey -> Signature
191```
193Sign a message. Unlike secp256k1/P-256, Ed25519 takes the **raw message bytes** (hex-encoded), not a pre-hash. Ed25519 hashes internally with SHA-512.
195```elm
196verify : Signature -> String -> PublicKey -> Bool
197```
199### Example
201```elm
202import Crypto.Ed25519 as Ed25519
204case Ed25519.privateKeyFromHex "9d61b19d...cae7f60" of
205 Just priv ->
206 let
207 pub = Ed25519.getPublicKey priv
208 sig = Ed25519.sign messageHex priv
209 in
210 Ed25519.verify sig messageHex pub -- True
212 Nothing ->
213 False
214```
216---
218## Crypto.X25519
220```elm
221import Crypto.X25519 as X25519
222```
224X25519 Diffie-Hellman key exchange on Curve25519 (Montgomery form). Used in WireGuard, Signal, Noise Protocol, TLS 1.3.
226### Types
228```elm
229type PrivateKey
230type PublicKey
231```
233### Key Conversion
235```elm
236privateKeyFromHex : String -> Maybe PrivateKey
237privateKeyToHex : PrivateKey -> String
238publicKeyFromHex : String -> Maybe PublicKey
239publicKeyToHex : PublicKey -> String
240```
242Parse hex-encoded keys (32 bytes / 64 hex chars).
244### Operations
246```elm
247getPublicKey : PrivateKey -> PublicKey
248```
250Derive the public key from a private key.
252```elm
253getSharedSecret : PrivateKey -> PublicKey -> String
254```
256ECDH: compute a shared secret from your private key and their public key. Returns the hex-encoded shared point (32 bytes / 64 hex chars). Both parties derive the same secret.
258### Example
260```elm
261import Crypto.X25519 as X25519
263case ( X25519.privateKeyFromHex alicePrivHex, X25519.privateKeyFromHex bobPrivHex ) of
264 ( Just alicePriv, Just bobPriv ) ->
265 let
266 alicePub = X25519.getPublicKey alicePriv
267 bobPub = X25519.getPublicKey bobPriv
268 secret1 = X25519.getSharedSecret alicePriv bobPub
269 secret2 = X25519.getSharedSecret bobPriv alicePub
270 in
271 secret1 == secret2 -- True
273 _ ->
274 False
275```
277---
279## Crypto.P256
281```elm
282import Crypto.P256 as P256
283```
285Identical API to `Crypto.Secp256k1` — same types and functions, different curve (NIST P-256 / secp256r1 / prime256v1).
287### Types
289```elm
290type PrivateKey
291type PublicKey
292type Signature
293```
295### Key Conversion
297```elm
298privateKeyFromHex : String -> Maybe PrivateKey
299privateKeyToHex : PrivateKey -> String
300publicKeyFromHex : String -> Maybe PublicKey
301publicKeyToHex : PublicKey -> String
302```
304### Signature Conversion
306```elm
307signatureFromCompactHex : String -> Maybe Signature
308signatureFromDerHex : String -> Maybe Signature
309signatureToCompactHex : Signature -> String
310signatureToDerHex : Signature -> String
311```
313### Operations
315```elm
316getPublicKey : PrivateKey -> PublicKey
317sign : String -> PrivateKey -> Signature
318verify : Signature -> String -> PublicKey -> Bool
319getSharedSecret : PrivateKey -> PublicKey -> String
320```
322---
324## Crypto.Cipher
326```elm
327import Crypto.Cipher as Cipher
328```
330Authenticated encryption (AEAD). Ciphertext includes a 16-byte authentication tag appended automatically. Decryption returns `Nothing` if authentication fails (tampered data or wrong key/nonce).
332### Types
334```elm
335type Key
336```
338A 256-bit (32-byte) symmetric key. Used by all three ciphers.
340```elm
341type Nonce12
342```
344A 96-bit (12-byte) nonce. Used by AES-GCM and ChaCha20-Poly1305.
346```elm
347type Nonce24
348```
350A 192-bit (24-byte) nonce. Used by XChaCha20-Poly1305. The larger nonce space makes random nonce generation safe (negligible collision probability).
352### Key & Nonce Construction
354```elm
355keyFromHex : String -> Maybe Key -- 32 bytes / 64 hex chars
356keyToHex : Key -> String
357nonce12FromHex : String -> Maybe Nonce12 -- 12 bytes / 24 hex chars
358nonce12ToHex : Nonce12 -> String
359nonce24FromHex : String -> Maybe Nonce24 -- 24 bytes / 48 hex chars
360nonce24ToHex : Nonce24 -> String
361```
363### AES-256-GCM
365```elm
366aesGcmEncrypt : Key -> Nonce12 -> String -> String
367```
369Encrypt plaintext (hex). Returns ciphertext + 16-byte auth tag (hex).
371```elm
372aesGcmDecrypt : Key -> Nonce12 -> String -> Maybe String
373```
375Decrypt ciphertext + tag (hex). Returns `Nothing` if authentication fails.
377### ChaCha20-Poly1305
379```elm
380chacha20Encrypt : Key -> Nonce12 -> String -> String
381chacha20Decrypt : Key -> Nonce12 -> String -> Maybe String
382```
384### XChaCha20-Poly1305
386```elm
387xchacha20Encrypt : Key -> Nonce24 -> String -> String
388xchacha20Decrypt : Key -> Nonce24 -> String -> Maybe String
389```
391### Example: Symmetric Encryption
393```elm
394import Crypto.Cipher as Cipher
396case ( Cipher.keyFromHex keyHex, Cipher.nonce12FromHex nonceHex ) of
397 ( Just key, Just nonce ) ->
398 let
399 ciphertext = Cipher.aesGcmEncrypt key nonce plaintextHex
400 in
401 case Cipher.aesGcmDecrypt key nonce ciphertext of
402 Just decrypted ->
403 decrypted == plaintextHex -- True
405 Nothing ->
406 False -- authentication failed
408 _ ->
409 False -- invalid key or nonce
410```
412### Example: ECDH + Encryption (Public-Key Encryption)
414Combine `Crypto.Secp256k1.getSharedSecret` with `Crypto.Cipher` for public-key encryption:
416```elm
417import Crypto.Secp256k1 as Secp256k1
418import Crypto.Cipher as Cipher
420-- Alice encrypts for Bob
421encryptForBob : Secp256k1.PrivateKey -> Secp256k1.PublicKey -> String -> String -> Maybe String
422encryptForBob alicePriv bobPub nonceHex plaintextHex =
423 let
424 shared = Secp256k1.getSharedSecret alicePriv bobPub
425 sharedKeyHex = String.left 64 shared -- first 32 bytes
426 in
427 case ( Cipher.keyFromHex sharedKeyHex, Cipher.nonce12FromHex nonceHex ) of
428 ( Just key, Just nonce ) ->
429 Just (Cipher.aesGcmEncrypt key nonce plaintextHex)
431 _ ->
432 Nothing
434-- Bob decrypts from Alice
435decryptFromAlice : Secp256k1.PrivateKey -> Secp256k1.PublicKey -> String -> String -> Maybe String
436decryptFromAlice bobPriv alicePub nonceHex ciphertextHex =
437 let
438 shared = Secp256k1.getSharedSecret bobPriv alicePub
439 sharedKeyHex = String.left 64 shared
440 in
441 case ( Cipher.keyFromHex sharedKeyHex, Cipher.nonce12FromHex nonceHex ) of
442 ( Just key, Just nonce ) ->
443 Cipher.aesGcmDecrypt key nonce ciphertextHex
445 _ ->
446 Nothing
447```
449Both sides derive the same shared secret, so Alice's encryption can be decrypted by Bob and vice versa.
451---
453## Nonce Safety
455**Never reuse a (key, nonce) pair.** Reusing a nonce with the same key completely breaks the security of all three ciphers.
457- For **AES-GCM** and **ChaCha20-Poly1305** (12-byte nonce): use a counter or derive nonces deterministically. Random 12-byte nonces have a non-negligible collision risk after ~2^32 messages.
458- For **XChaCha20-Poly1305** (24-byte nonce): random nonces are safe. The 24-byte nonce space makes collision negligible even after billions of messages.
460## Hex Encoding
462All functions accept and return **lowercase hex strings**. One byte = two hex characters.
464| Data | Hex length |
465|---|---|
466| Private key (32 bytes) | 64 chars |
467| Public key, compressed (33 bytes) | 66 chars |
468| Public key, uncompressed (65 bytes) | 130 chars |
469| Ed25519 public key (32 bytes) | 64 chars |
470| X25519 public key (32 bytes) | 64 chars |
471| Signature, compact (64 bytes) | 128 chars |
472| Cipher key (32 bytes) | 64 chars |
473| Nonce12 (12 bytes) | 24 chars |
474| Nonce24 (24 bytes) | 48 chars |
476## License
478MIT (noble-curves, noble-ciphers), BSD-3-Clause (lynxjs-elm/crypto)