Skip to content

Conversation

@bbarker
Copy link
Contributor

@bbarker bbarker commented Jan 7, 2026

Overview

  • What does this change accomplish and why?

    • Adds Argon2id password hashing support as builtins, enabling secure password storage in Unison applications
    • Argon2id is the recommended password hashing algorithm per OWASP guidelines and won the Password Hashing Competition
    • Migrates from cryptonite to crypton (its actively-maintained fork) across the codebase
    • Accompanying @unison/base contribution - not sure if the diff is stalling due to failures in depending on unreleased unison code or for other reasons
  • How does it change the user experience?

    • Users can now hash passwords securely and verify them against stored hashes
    • Two new builtins in the crypto.argon2 namespace
    -- Hash with user-provided options and salt (raw bytes API)
    crypto.argon2.hashRaw : Nat -> Nat -> Nat -> Nat -> Bytes -> Bytes -> Either Failure Bytes
    -- Args: memory (KiB), iterations, parallelism, outputLen, password, salt
    -- Returns: raw hash bytes
    
    -- Verify password against raw hash
    crypto.argon2.verifyRaw : Nat -> Nat -> Nat -> Bytes -> Bytes -> Bytes -> Either Failure Boolean
    -- Args: memory (KiB), iterations, parallelism, password, salt, expectedHash
    -- Returns: Right True if match, Right False if mismatch, Left Failure on error
    
  • Before and after examples:

    Hashing a password (with OWASP-recommended settings):

    password = Text.toUtf8 "my-secret-password"
    salt = Text.toUtf8 "16bytesalt12345!"  -- 16 bytes minimum
    
    -- Hash with memory=47104 KiB, iterations=1, parallelism=1, output=32 bytes
    hash = crypto.argon2.hashRaw 47104 1 1 32 password salt
    -- Result: Right 0xscf12689adbeb0f1a921e87a22393f1d6acf8cdf3d9ad6bbb99561f1918ef55c9
    

    Verifying a password:

    expectedHash = 0xscf12689adbeb0f1a921e87a22393f1d6acf8cdf3d9ad6bbb99561f1918ef55c9
    
    isValid = crypto.argon2.verifyRaw 47104 1 1 password salt expectedHash
    -- Result: Right true
    
    isWrong = crypto.argon2.verifyRaw 47104 1 1 (Text.toUtf8 "wrong") salt expectedHash
    -- Result: Right false
    

Implementation approach and notes

  1. Migrated from cryptonite to crypton across the codebase

    • crypton is the actively-maintained fork of cryptonite (which is no longer maintained)
    • crypton provides Argon2 support via Crypto.KDF.Argon2
    • Updated all package.yaml files: unison-core, unison-syntax, unison-runtime, unison-hashing-v2, unison-cli
  2. Defined ForeignFunc enum variants in Type.hs:

    • Crypto_Argon2_HashRaw
    • Crypto_Argon2_VerifyRaw
  3. Implemented foreign functions in Function.hs:

    • argon2HashRawWrapper - Calls Argon2.hash with user options/salt, returns raw hash bytes
    • argon2VerifyRawWrapper - Re-hashes password with same parameters and does constant-time comparison
    • argon2ErrMsg - Maps all CryptoError codes to human-readable messages
  4. Added 6-tuple ForeignConvention support - The hash function takes 6 arguments (4 options + password + salt), which required adding:

    • pattern Tup5V
    • pattern Tup6C
    • decodeTup6 / encodeTup6
    • ForeignConvention (a,b,c,d,e,f) instance

Interesting/controversial decisions

  1. Argon2id only - Only exposed Argon2id variant (not Argon2i or Argon2d). Argon2id is the recommended hybrid approach that provides both side-channel resistance and GPU/ASIC resistance.

  2. Individual Nat arguments vs options struct - Chose individual Nat arguments for the builtins rather than a struct. This keeps the builtins simple; wrapper functions in @unison/base can provide a nicer Argon2Options struct API.

  3. Raw bytes API - The builtins work with raw bytes rather than PHC-encoded strings. This is the lower-level API; PHC encoding can be added in @unison/base wrapper functions if needed.

  4. Either Failure Boolean for verify - verifyRaw returns Either Failure Boolean rather than just Boolean. This allows callers to distinguish between:

    • Right True - password matches
    • Right False - password doesn't match (not an error)
    • Left Failure - crypto error (e.g., invalid parameters)

    This is important because a crypto error (like invalid salt size) is a data integrity issue worth logging, whereas a wrong password is expected behavior.

  5. 6-tuple support - Added general 6-tuple ForeignConvention rather than using nested tuples, as it's cleaner and may benefit other future builtins.

  6. cryptonite to crypton migration - Took the opportunity to migrate the entire codebase from the unmaintained cryptonite to its actively-maintained fork crypton, which has an identical API.

Test coverage

  • Transcript test added: unison-src/transcripts/idempotent/argon2.md

    • Tests basic hashing with OWASP-recommended settings
    • Tests verification with correct password (returns Right true)
    • Tests verification with wrong password (returns Right false)
    • Tests error handling (salt too short returns Left Failure with descriptive message)
  • Additional test coverage could include:

    • Haskell unit tests for edge cases (empty password, maximum sizes, etc.)
    • Property tests for hash/verify round-trip

Loose ends

  • @unison/base wrappers - Convenience functions with default options, an Argon2Options type, and PHC encoding can be added to @unison/base in a follow-up PR to that repo
  • Documentation - Crypto documentation should be updated to include argon2

Final checklist

  • PR title: Add Argon2id builtins; migrate cryptonite to crypton
  • Transcripts included: unison-src/transcripts/idempotent/argon2.md
  • package.yaml files updated (not .cabal files directly)

@bbarker
Copy link
Contributor Author

bbarker commented Jan 7, 2026

@pchiusano I know we'd spoken previously about adding some more cryptographic support, though this is a little different than what we'd discussed.

@github-actions
Copy link

github-actions bot commented Jan 7, 2026

Some tests with 'continue-on-error: true' have failed:

  • Cabal / smoke-test

Created by continue-on-error-comment

@aryairani
Copy link
Contributor

Different how?

@bbarker
Copy link
Contributor Author

bbarker commented Jan 7, 2026

Different how?

Originally we'd discussed AES-256

@pchiusano
Copy link
Member

I'm good with this. Argon2 seems like the gold standard these days, however I'd prefer to get the implementation from the existing cryptonite library that we already depend on rather than a new C library.

Is there any reason not to use: https://hackage.haskell.org/package/cryptonite-0.30/docs/Crypto-KDF-Argon2.html

Or we could switch to the fork, crypton, which is actively maintained.

@bbarker
Copy link
Contributor Author

bbarker commented Jan 12, 2026

I think it is reasonable to go with crypton/(ite) - if we switch to crypton, do you want to convert other usages from cryptonite to crypton in the same PR? PHC

The only tradeoff is we'd have to write our own PHC codec as I don't think crypton has that, but could do that. PHC is handy since embedding params in the PHC string means you can verify hashes even if you change default params later. But, we could do this in Unison (base) and that should actually keep the Haskell side lighter.

@pchiusano
Copy link
Member

Up to you. If crypton is really a drop-in replacement as advertised, no objections to upgrading to that everywhere.

Also fine to do everything with cryptonite if that works fine and leave the upgrade to crypton for a later PR.

@bbarker
Copy link
Contributor Author

bbarker commented Jan 13, 2026

@aryairani @pchiusano should be ready for review

@bbarker bbarker changed the title [Draft] Add Argon2id password hashing builtins Add Argon2id password hashing builtins; switch from cryptonite to crypton Jan 13, 2026
- bytes
- containers
- cryptonite
- crypton
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since I was curious:

While cryptonite has a obvious bug, it has not been fixed for a long time. This is because the author is now inactive in the Haskell community. In private communication with him, I was not given the upload permission of cryptonite to Hackage. He suggested me to fork cryponite and upload it as a different package.
So, I created crypton and uploaded it to Hackage with the bug fixed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I am retroactively curious and my curiosity is satisfied.

@ChrisPenner
Copy link
Member

Just chiming in that yes, crypton is the recommended lib for this now, cryptonite is deprecated 👍🏼

@aryairani aryairani merged commit 9def7d6 into unisonweb:trunk Jan 20, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants