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) }
}
|