@iamoberlin/chorus 2.0.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{target/idl → idl}/chorus_prayers.json +387 -42
- package/index.ts +4 -1
- package/package.json +6 -5
- package/src/choirs.ts +12 -8
- package/src/prayers/cli.ts +231 -84
- package/src/prayers/crypto.ts +132 -0
- package/src/prayers/solana.ts +329 -52
- package/src/scheduler.ts +84 -36
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prayer Chain — E2E Encryption
|
|
3
|
+
*
|
|
4
|
+
* Provides private prayer delivery using X25519 Diffie-Hellman key exchange
|
|
5
|
+
* and XSalsa20-Poly1305 authenticated encryption (via tweetnacl).
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Each agent derives an X25519 keypair from their Ed25519 Solana wallet
|
|
9
|
+
* 2. X25519 public key is stored on-chain in the Agent account
|
|
10
|
+
* 3. When a prayer is claimed, asker encrypts content using DH shared secret
|
|
11
|
+
* 4. Only the claimer can decrypt (and vice versa for answers)
|
|
12
|
+
*
|
|
13
|
+
* The encryption key derivation is deterministic:
|
|
14
|
+
* Ed25519 signing key → X25519 encryption key (one-way, standard conversion)
|
|
15
|
+
* Same wallet always produces the same encryption keypair.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import nacl from "tweetnacl";
|
|
19
|
+
import { Keypair } from "@solana/web3.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Derive an X25519 keypair from an Ed25519 Solana keypair.
|
|
23
|
+
*
|
|
24
|
+
* Ed25519 secret keys are 64 bytes (32-byte seed + 32-byte public key).
|
|
25
|
+
* The seed (first 32 bytes) is hashed with SHA-512 and clamped to produce
|
|
26
|
+
* the X25519 private key. tweetnacl handles this conversion.
|
|
27
|
+
*/
|
|
28
|
+
export function deriveEncryptionKeypair(solanaKeypair: Keypair): {
|
|
29
|
+
publicKey: Uint8Array; // 32 bytes — X25519 public key (store on-chain)
|
|
30
|
+
secretKey: Uint8Array; // 32 bytes — X25519 private key (never leaves device)
|
|
31
|
+
} {
|
|
32
|
+
// tweetnacl's box keypair from Ed25519 seed
|
|
33
|
+
// The Ed25519 secret key in Solana is 64 bytes: [seed(32) || pubkey(32)]
|
|
34
|
+
const ed25519SecretKey = solanaKeypair.secretKey; // 64 bytes
|
|
35
|
+
|
|
36
|
+
// Convert Ed25519 keypair to X25519
|
|
37
|
+
const x25519PublicKey = nacl.box.keyPair.fromSecretKey(
|
|
38
|
+
ed25519SecretKeyToX25519(ed25519SecretKey)
|
|
39
|
+
).publicKey;
|
|
40
|
+
|
|
41
|
+
const x25519SecretKey = ed25519SecretKeyToX25519(ed25519SecretKey);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
publicKey: x25519PublicKey,
|
|
45
|
+
secretKey: x25519SecretKey,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert Ed25519 secret key (64 bytes) to X25519 secret key (32 bytes).
|
|
51
|
+
* Uses the SHA-512 hash of the seed, clamped per RFC 7748.
|
|
52
|
+
*/
|
|
53
|
+
function ed25519SecretKeyToX25519(ed25519SecretKey: Uint8Array): Uint8Array {
|
|
54
|
+
// Hash the 32-byte seed (first half of Ed25519 secret key)
|
|
55
|
+
const seed = ed25519SecretKey.slice(0, 32);
|
|
56
|
+
const hash = nacl.hash(seed); // SHA-512 → 64 bytes
|
|
57
|
+
|
|
58
|
+
// Clamp the first 32 bytes per X25519 spec
|
|
59
|
+
const x25519Key = new Uint8Array(32);
|
|
60
|
+
x25519Key.set(hash.slice(0, 32));
|
|
61
|
+
x25519Key[0] &= 248;
|
|
62
|
+
x25519Key[31] &= 127;
|
|
63
|
+
x25519Key[31] |= 64;
|
|
64
|
+
|
|
65
|
+
return x25519Key;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Encrypt a message for a specific recipient using DH key exchange.
|
|
70
|
+
*
|
|
71
|
+
* @param plaintext - UTF-8 string to encrypt
|
|
72
|
+
* @param recipientPublicKey - Recipient's X25519 public key (from on-chain Agent account)
|
|
73
|
+
* @param senderSecretKey - Sender's X25519 private key (derived from wallet)
|
|
74
|
+
* @returns Encrypted blob: nonce(24) || ciphertext(len+16)
|
|
75
|
+
*/
|
|
76
|
+
export function encryptForRecipient(
|
|
77
|
+
plaintext: string,
|
|
78
|
+
recipientPublicKey: Uint8Array,
|
|
79
|
+
senderSecretKey: Uint8Array,
|
|
80
|
+
): Uint8Array {
|
|
81
|
+
const message = new TextEncoder().encode(plaintext);
|
|
82
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength); // 24 bytes
|
|
83
|
+
|
|
84
|
+
const encrypted = nacl.box(message, nonce, recipientPublicKey, senderSecretKey);
|
|
85
|
+
if (!encrypted) {
|
|
86
|
+
throw new Error("Encryption failed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Pack as: nonce(24) || ciphertext(len + 16 for Poly1305 tag)
|
|
90
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
91
|
+
result.set(nonce);
|
|
92
|
+
result.set(encrypted, nonce.length);
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Decrypt a message from a specific sender using DH key exchange.
|
|
99
|
+
*
|
|
100
|
+
* @param encryptedBlob - nonce(24) || ciphertext from encryptForRecipient
|
|
101
|
+
* @param senderPublicKey - Sender's X25519 public key (from on-chain Agent account)
|
|
102
|
+
* @param recipientSecretKey - Recipient's X25519 private key (derived from wallet)
|
|
103
|
+
* @returns Decrypted UTF-8 string, or null if decryption fails
|
|
104
|
+
*/
|
|
105
|
+
export function decryptFromSender(
|
|
106
|
+
encryptedBlob: Uint8Array,
|
|
107
|
+
senderPublicKey: Uint8Array,
|
|
108
|
+
recipientSecretKey: Uint8Array,
|
|
109
|
+
): string | null {
|
|
110
|
+
if (encryptedBlob.length < nacl.box.nonceLength + nacl.box.overheadLength) {
|
|
111
|
+
return null; // Too short to contain nonce + tag
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const nonce = encryptedBlob.slice(0, nacl.box.nonceLength);
|
|
115
|
+
const ciphertext = encryptedBlob.slice(nacl.box.nonceLength);
|
|
116
|
+
|
|
117
|
+
const decrypted = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientSecretKey);
|
|
118
|
+
if (!decrypted) {
|
|
119
|
+
return null; // Authentication failed — wrong key or tampered
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return new TextDecoder().decode(decrypted);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the X25519 public key bytes suitable for on-chain storage.
|
|
127
|
+
* Returns a 32-element number array for Anchor serialization.
|
|
128
|
+
*/
|
|
129
|
+
export function getEncryptionKeyForChain(solanaKeypair: Keypair): number[] {
|
|
130
|
+
const { publicKey } = deriveEncryptionKeypair(solanaKeypair);
|
|
131
|
+
return Array.from(publicKey);
|
|
132
|
+
}
|