Skip to content

Complete RFC 9580 crypto modernization (Argon2 S2K + 256-bit keys) #3369

@ahenry-i-tracing

Description

@ahenry-i-tracing

Complete RFC 9580 crypto modernization (Argon2 S2K + 256-bit keys)

Context

Thank you for the swift merge of #3364.

The migration to ProtonMail/go-crypto and the activation of AEAD (SEIPDv2/GCM) is a major security improvement. Yopass now uses modern authenticated encryption.

Two low-hanging improvements remain to fully align with the OpenPGP crypto refresh (RFC 9580) and maximize the cryptographic strength of Yopass:

  1. Argon2 for String-to-Key (S2K): replace the legacy iterated SHA-256 with memory-hard Argon2
  2. 256-bit encryption keys: increase from 131 bits (22 chars) to 256 bits (43 chars)

Both changes are backward-compatible: existing secrets remain readable, only new secrets benefit.


1. Argon2 for S2K key derivation

Current behavior

OpenPGP.js defaults to S2K type 3 (iterated and salted SHA-256, ~65 million iterations). This is CPU-only and theoretically vulnerable to GPU/ASIC bruteforce attacks, though Yopass's auto-generated keys mitigate this in practice.

Proposed change

Frontend: website/src/shared/lib/crypto.ts:

export const encryptionConfig: Partial<Config> = {
  aeadProtect: true,
  preferredAEADAlgorithm: enums.aead.gcm,
  s2kType: enums.s2k.argon2,  // ← add this
};

Server: pkg/yopass/yopass.go (for CLI --encrypt consistency):

import "github.com/ProtonMail/go-crypto/openpgp/s2k"

var pgpConfig = &packet.Config{
    DefaultHash:            crypto.SHA256,
    DefaultCipher:          packet.CipherAES256,
    DefaultCompressionAlgo: packet.CompressionNone,
    AEADConfig:             &packet.AEADConfig{DefaultMode: packet.AEADModeGCM},
    S2KConfig:              &s2k.Config{S2KMode: s2k.Argon2S2K},  // ← add this
}

No change is needed for CLI --decrypt: ProtonMail/go-crypto v1.4.1 auto-detects the S2K type from the message and already supports Argon2 (S2K type 4).

Why?

Property Iterated S2K (type 3) Argon2 (type 4)
Algorithm SHA-256, ~65M iterations Argon2id (memory-hard)
GPU/ASIC resistance Low: SHA-256 is fast on dedicated hardware High: memory-bound
RFC 9580 status Legacy Recommended
OpenPGP.js 6.x support
go-crypto v1.4.1 support

This matters especially for users who supply custom passphrases via --key instead of using the auto-generated key, since custom passphrases typically have lower entropy.


2. Increase key entropy to 256 bits

Current behavior

GenerateKey() in pkg/yopass/yopass.go:

func GenerateKey() (string, error) {
    const length = 22
    b := make([]byte, length)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(b)[:length], nil
}

This generates 22 random bytes, base64url-encodes them, then truncates to 22 characters; yielding approximately 131 bits of entropy. While 131 bits is strong, it does not match the 256-bit AES key size.

Proposed change

func GenerateKey() (string, error) {
    b := make([]byte, 32) // 256 bits
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(b), nil // 43 chars, no padding
}

And the corresponding test in yopass_test.go:

format := regexp.MustCompile("^[a-zA-Z0-9-_]{43}$")

Impact on URLs

Before (131 bits, 22 chars):
https://yopass.se/#/s/3ae57dce-16e8-4ee5-a09a-98341a84ffbc/aB3kXvTj96RMZXCJwPcn5a

After (256 bits, 43 chars):
https://yopass.se/#/s/3ae57dce-16e8-4ee5-a09a-98341a84ffbc/xK9m2Fh7vNqYpR3wT8bZjL5sA0dGcE6iUoXnHyWfBkQ

The URL grows by ~21 characters. It remains copy-pasteable, QR-codable, and compatible with all messaging channels.

Why?

Matching the key entropy to the cipher key size is a cryptographic best practice. AES-256 provides 256 bits of security, but if the passphrase only has 131 bits of entropy, the effective security is bounded at 131 bits. With 256-bit keys, the passphrase strength matches the cipher, and the full security margin of AES-256 is utilized.


Backward compatibility

Both changes are fully backward-compatible:

Scenario Behavior
Old secret (S2K type 3, 22-char key) read by updated Yopass ✅ Works: S2K type auto-detected, key length irrelevant for decryption
New secret (Argon2, 43-char key) read by older Yopass ⚠️ Requires OpenPGP.js ≥ 5.x and go-crypto (both support Argon2). Older OpenPGP.js v4 cannot read Argon2 messages.
Mixed deployment during rollout Old clients can't decrypt new secrets, but this is already the case with AEAD from #3364

Since #3364 already introduced a format change (AEAD/SEIPDv2), adding Argon2 + longer keys in the same release cycle does not create additional compatibility concerns.


Summary

Change Effort Impact
Argon2 S2K 1 line frontend + 2 lines Go Memory-hard KDF, GPU/ASIC resistant
256-bit keys 3 lines Go + test update Full AES-256 security margin

Together with the AEAD migration from #3364, these changes would give Yopass a complete RFC 9580 cryptographic profile:

  • ✅ AES-256-GCM (AEAD, authenticated encryption)
  • ✅ Argon2 (memory-hard key derivation)
  • ✅ 256-bit keys (full security margin)
  • ProtonMail/go-crypto (actively maintained)

Would be happy to submit a PR if helpful.

Thank you in advance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions