aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/keys/Kdf.kt
blob: 30f094ee582ead06d4a75221064eaa62b0c385d3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package org.meshbay.client.keys

import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
import org.bouncycastle.crypto.params.Argon2Parameters
import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters
import org.bouncycastle.crypto.params.HKDFParameters
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters
import java.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

/**
 * The primitives keyring.js takes from node:crypto and the vendored Argon2,
 * with the same numbers. keyderive.js (the page), keyring.js (desktop) and this
 * are one format: a mismatch looks like an account nobody can open, not like
 * an error. meshbay-hub/tests/vectors/keyring.json holds them together.
 */
object Kdf {
    // keyderive.js: the same numbers, or no bundle opens across the clients.
    const val ARGON2_MEMORY_KIB = 131072
    const val ARGON2_PASSES = 3
    const val ARGON2_PARALLELISM = 1
    const val ARGON2_TAG = 32

    private val ED_PKCS8_PREFIX = hex("302e020100300506032b657004220420")
    private val X_PKCS8_PREFIX = hex("302e020100300506032b656e04220420")

    // One derivation at a time: 128 MiB each, on a phone. (Two concurrent
    // lanes=4 derivations deadlock inside OpenSSL on the hub — CLAUDE.md; not
    // this library, but there is no reason to find out.)
    @Synchronized
    fun argon2id(password: String, salt: ByteArray): ByteArray {
        val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
            .withVersion(Argon2Parameters.ARGON2_VERSION_13)
            .withIterations(ARGON2_PASSES)
            .withMemoryAsKB(ARGON2_MEMORY_KIB)
            .withParallelism(ARGON2_PARALLELISM)
            .withSalt(salt)
            .build()
        val gen = Argon2BytesGenerator()
        gen.init(params)
        val out = ByteArray(ARGON2_TAG)
        gen.generateBytes(password.toByteArray(Charsets.UTF_8), out)
        return out
    }

    /** node:crypto hkdfSync('sha256', ikm, <empty salt>, info, 32). */
    fun hkdf(ikm: ByteArray, info: String): ByteArray {
        val gen = HKDFBytesGenerator(SHA256Digest())
        gen.init(HKDFParameters(ikm, null, info.toByteArray(Charsets.UTF_8)))
        val out = ByteArray(32)
        gen.generateBytes(out, 0, 32)
        return out
    }

    fun sha256(data: ByteArray): ByteArray {
        val d = SHA256Digest()
        d.update(data, 0, data.size)
        val out = ByteArray(32)
        d.doFinal(out, 0)
        return out
    }

    /** AES-256-GCM, 16-byte tag appended — the layout node:crypto's getAuthTag gives. */
    fun gcmSeal(key: ByteArray, nonce: ByteArray, plain: ByteArray, aad: ByteArray?): ByteArray {
        val c = Cipher.getInstance("AES/GCM/NoPadding")
        c.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce))
        if (aad != null) c.updateAAD(aad)
        return c.doFinal(plain)
    }

    fun gcmOpen(key: ByteArray, nonce: ByteArray, ctAndTag: ByteArray, aad: ByteArray?): ByteArray {
        val c = Cipher.getInstance("AES/GCM/NoPadding")
        c.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, nonce))
        if (aad != null) c.updateAAD(aad)
        return c.doFinal(ctAndTag)
    }

    // Keys are stored as Node exports them: PKCS#8 DER, RFC 8410, 48 bytes, no
    // public key attached. Written out by hand because a library's own PKCS#8
    // encoder may add the optional public key, and the stored format is one.
    fun edToPkcs8(k: Ed25519PrivateKeyParameters) = ED_PKCS8_PREFIX + k.encoded
    fun xToPkcs8(k: X25519PrivateKeyParameters) = X_PKCS8_PREFIX + k.encoded

    fun edFromPkcs8(der: ByteArray): Ed25519PrivateKeyParameters {
        require(der.size == 48 && der.copyOfRange(0, 16).contentEquals(ED_PKCS8_PREFIX)) {
            "not an Ed25519 PKCS#8 key"
        }
        return Ed25519PrivateKeyParameters(der, 16)
    }

    fun xFromPkcs8(der: ByteArray): X25519PrivateKeyParameters {
        require(der.size == 48 && der.copyOfRange(0, 16).contentEquals(X_PKCS8_PREFIX)) {
            "not an X25519 PKCS#8 key"
        }
        return X25519PrivateKeyParameters(der, 16)
    }

    fun b64(b: ByteArray): String = Base64.getEncoder().encodeToString(b)
    fun unb64(s: String?): ByteArray = Base64.getDecoder().decode(s ?: "")
    fun hex(s: String): ByteArray = ByteArray(s.length / 2) { s.substring(2 * it, 2 * it + 2).toInt(16).toByte() }
    fun toHex(b: ByteArray): String = b.joinToString("") { "%02x".format(it) }
}