@qbix/q 1.0.5
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/LICENSE +21 -0
- package/README.md +678 -0
- package/dist/Metrics.js +2873 -0
- package/dist/Metrics.min.js +95 -0
- package/dist/Q.js +15677 -0
- package/dist/Q.min.js +385 -0
- package/dist/Q.minimal.js +11594 -0
- package/dist/Q.minimal.min.js +286 -0
- package/dist/handlebars-v4.0.10.min.js +29 -0
- package/dist/handlebars.minimal.min.js +1 -0
- package/dist/img/hints/rotate-left.gif +0 -0
- package/dist/img/hints/swipe-down.gif +0 -0
- package/dist/img/hints/swipe-up.gif +0 -0
- package/dist/img/hints/tap.gif +0 -0
- package/dist/img/throbbers/loading.gif +0 -0
- package/dist/jquery.minimal.min.js +18 -0
- package/dist/methods/Q/Audio/load.js +29 -0
- package/dist/methods/Q/Audio/loadVoices.js +36 -0
- package/dist/methods/Q/Audio/play.js +47 -0
- package/dist/methods/Q/Audio/speak.js +149 -0
- package/dist/methods/Q/Crypto/delegate.js +186 -0
- package/dist/methods/Q/Crypto/internalKeypair.js +170 -0
- package/dist/methods/Q/Crypto/sign.js +200 -0
- package/dist/methods/Q/Crypto/verify.js +212 -0
- package/dist/methods/Q/Crypto/verifyDelegated.js +214 -0
- package/dist/methods/Q/Data/Bloom/_internal.js +163 -0
- package/dist/methods/Q/Data/Bloom/create.js +23 -0
- package/dist/methods/Q/Data/Bloom/fromBase64.js +21 -0
- package/dist/methods/Q/Data/Bloom/fromBytes.js +15 -0
- package/dist/methods/Q/Data/Bloom/fromElements.js +33 -0
- package/dist/methods/Q/Data/Merkle/_internal.js +68 -0
- package/dist/methods/Q/Data/Merkle/build.js +29 -0
- package/dist/methods/Q/Data/Merkle/proof.js +50 -0
- package/dist/methods/Q/Data/Merkle/verify.js +32 -0
- package/dist/methods/Q/Data/Prolly/_internal.js +190 -0
- package/dist/methods/Q/Data/Prolly/build.js +28 -0
- package/dist/methods/Q/Data/Prolly/delete.js +28 -0
- package/dist/methods/Q/Data/Prolly/diff.js +70 -0
- package/dist/methods/Q/Data/Prolly/get.js +38 -0
- package/dist/methods/Q/Data/Prolly/set.js +32 -0
- package/dist/methods/Q/Data/compress.js +45 -0
- package/dist/methods/Q/Data/decompress.js +35 -0
- package/dist/methods/Q/Data/decrypt.js +55 -0
- package/dist/methods/Q/Data/derive.js +76 -0
- package/dist/methods/Q/Data/digest.js +29 -0
- package/dist/methods/Q/Data/encrypt.js +61 -0
- package/dist/methods/Q/Data/generateKey.js +40 -0
- package/dist/methods/Q/Data/hkdf.js +44 -0
- package/dist/methods/Q/Data/importKey.js +34 -0
- package/dist/methods/Q/Data/sign.js +42 -0
- package/dist/methods/Q/Data/verify.js +45 -0
- package/dist/methods/Q/Onboarding/handle.js +50 -0
- package/dist/methods/Q/Onboarding/start.js +165 -0
- package/dist/methods/Q/Onboarding/stop.js +21 -0
- package/dist/methods/Q/Sandbox/run.js +392 -0
- package/dist/methods/Q/Tool/define/component.js +218 -0
- package/dist/methods/Q/globalMemoryWalk.js +82 -0
- package/dist/methods/Q/leaves.js +34 -0
- package/dist/methods/Q/registerWebComponent.js +0 -0
- package/dist/methods/Q/sanitize.js +142 -0
- package/dist/test.html +6 -0
- package/dist/tools/Q/lazyload.js +433 -0
- package/package.json +26 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Perform a cryptographic delegation ceremony.
|
|
5
|
+
*
|
|
6
|
+
* Delegation is capability-based:
|
|
7
|
+
* - A child secret is deterministically derived from a parent secret
|
|
8
|
+
* - The parent signs a typed delegation statement
|
|
9
|
+
* - The result can be verified and chained
|
|
10
|
+
*
|
|
11
|
+
* SECURITY MODEL:
|
|
12
|
+
* - rootSecret is never returned
|
|
13
|
+
* - secret is the sole bearer of delegated capability
|
|
14
|
+
* - Authority is proven by the parent's signature
|
|
15
|
+
* - context is treated as an opaque, signed string
|
|
16
|
+
*
|
|
17
|
+
* SIGNING FORMATS:
|
|
18
|
+
* - "ES256" → P-256 + SHA-256. Digest = SHA-256(canonical JSON payload).
|
|
19
|
+
* - "EIP712" → secp256k1 + keccak256 + full EIP-712 struct encoding.
|
|
20
|
+
* Domain: { name: "Q.Crypto", version: "1", salt: keccak256(label) }
|
|
21
|
+
* The label is in the domain salt, making cross-label signature
|
|
22
|
+
* replay structurally impossible at the cryptographic level.
|
|
23
|
+
*
|
|
24
|
+
* @static
|
|
25
|
+
* @method delegate
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} options
|
|
28
|
+
* @param {Uint8Array} options.rootSecret Parent secret material.
|
|
29
|
+
* @param {String} options.label Delegation label (domain-separated).
|
|
30
|
+
* @param {String} [options.context] Opaque, signed context string.
|
|
31
|
+
* @param {String} [options.format='es256'] "ES256" or "EIP712".
|
|
32
|
+
*
|
|
33
|
+
* @return {Q.Promise<Object>}
|
|
34
|
+
* @return {String} return.label Delegation label.
|
|
35
|
+
* @return {String} return.context Signed context string.
|
|
36
|
+
* @return {Uint8Array} return.secret Delegated capability secret.
|
|
37
|
+
* @return {Object} return.statement The signed statement.
|
|
38
|
+
* @return {Object} return.proof Signed delegation proof.
|
|
39
|
+
*/
|
|
40
|
+
return function Q_Crypto_delegate(options) {
|
|
41
|
+
|
|
42
|
+
return new Q.Promise(async function (resolve, reject) {
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
|
|
46
|
+
if (!options) {
|
|
47
|
+
throw new Error("options required");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rootSecret = options.rootSecret;
|
|
51
|
+
const label = options.label;
|
|
52
|
+
const context = options.context;
|
|
53
|
+
const format = options.format || "ES256";
|
|
54
|
+
|
|
55
|
+
if (!(rootSecret instanceof Uint8Array)) {
|
|
56
|
+
throw new Error("rootSecret must be Uint8Array");
|
|
57
|
+
}
|
|
58
|
+
if (typeof label !== "string" || !label.length) {
|
|
59
|
+
throw new Error("label required");
|
|
60
|
+
}
|
|
61
|
+
if (context !== undefined && typeof context !== "string") {
|
|
62
|
+
throw new Error("context must be a string if provided");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const normalizedContext = context || "";
|
|
66
|
+
|
|
67
|
+
// -------------------------------------------------
|
|
68
|
+
// Derive delegated capability secret
|
|
69
|
+
// -------------------------------------------------
|
|
70
|
+
const derivedSecret = await Q.Data.derive(
|
|
71
|
+
rootSecret,
|
|
72
|
+
"q.crypto.delegate." + label,
|
|
73
|
+
{ size: 32 }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// -------------------------------------------------
|
|
77
|
+
// Derive parent keypair + identity
|
|
78
|
+
// -------------------------------------------------
|
|
79
|
+
const parentKp = await Q.Crypto.internalKeypair({
|
|
80
|
+
secret: rootSecret,
|
|
81
|
+
format: format
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
let parentIdentity;
|
|
85
|
+
let parentType;
|
|
86
|
+
|
|
87
|
+
if (format === "EIP712") {
|
|
88
|
+
// eip712: parent identity is the Ethereum address
|
|
89
|
+
parentIdentity = parentKp.address;
|
|
90
|
+
parentType = "address";
|
|
91
|
+
} else {
|
|
92
|
+
// es256: parent identity is SHA-256 of the public key
|
|
93
|
+
const pubBytes = Q.Data.toUint8Array(parentKp.publicKey);
|
|
94
|
+
const pubDigest = await Q.Data.digest("SHA-256", pubBytes);
|
|
95
|
+
parentIdentity = Q.Data.toHex(pubDigest);
|
|
96
|
+
parentType = "bytes32";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -------------------------------------------------
|
|
100
|
+
// Compute secret hash
|
|
101
|
+
// eip712: keccak256(derivedSecret)
|
|
102
|
+
// es256: SHA-256(derivedSecret)
|
|
103
|
+
// -------------------------------------------------
|
|
104
|
+
const { keccak_256 } = await import(
|
|
105
|
+
Q.url("{{Q}}/src/js/crypto/sha3.js")
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
let secretHashHex;
|
|
109
|
+
|
|
110
|
+
if (format === "EIP712") {
|
|
111
|
+
secretHashHex = Q.Data.toHex(keccak_256(derivedSecret));
|
|
112
|
+
} else {
|
|
113
|
+
secretHashHex = Q.Data.toHex(
|
|
114
|
+
await Q.Data.digest("SHA-256", derivedSecret)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// -------------------------------------------------
|
|
119
|
+
// Construct delegation statement (protocol-fixed)
|
|
120
|
+
// -------------------------------------------------
|
|
121
|
+
const statement = {
|
|
122
|
+
parent: parentIdentity,
|
|
123
|
+
label: label,
|
|
124
|
+
issuedTime: Math.floor(Date.now() / 1000),
|
|
125
|
+
context: normalizedContext,
|
|
126
|
+
secretHash: secretHashHex
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// -------------------------------------------------
|
|
130
|
+
// Build domain
|
|
131
|
+
// eip712: { name, version, salt } where
|
|
132
|
+
// salt = keccak256(utf8(label)) as bytes32.
|
|
133
|
+
// Placing the label in the domain salt makes
|
|
134
|
+
// cross-label replay impossible at the digest level.
|
|
135
|
+
// es256: domain is unused (empty object).
|
|
136
|
+
// -------------------------------------------------
|
|
137
|
+
const domain = format === "EIP712"
|
|
138
|
+
? {
|
|
139
|
+
name: "Q.Crypto",
|
|
140
|
+
version: "1",
|
|
141
|
+
salt: Q.Data.toHex(keccak_256(new TextEncoder().encode(label)))
|
|
142
|
+
}
|
|
143
|
+
: {};
|
|
144
|
+
|
|
145
|
+
// -------------------------------------------------
|
|
146
|
+
// Sign via Q.Crypto.sign()
|
|
147
|
+
// eip712 path uses hashTypedData() from eip712.js.
|
|
148
|
+
// es256 path uses SHA-256(canonical JSON).
|
|
149
|
+
// -------------------------------------------------
|
|
150
|
+
const proof = await Q.Crypto.sign({
|
|
151
|
+
secret: rootSecret,
|
|
152
|
+
domain: domain,
|
|
153
|
+
message: statement,
|
|
154
|
+
types: {
|
|
155
|
+
EIP712Domain: [
|
|
156
|
+
{ name: "name", type: "string" },
|
|
157
|
+
{ name: "version", type: "string" },
|
|
158
|
+
{ name: "salt", type: "bytes32" }
|
|
159
|
+
],
|
|
160
|
+
Delegation: [
|
|
161
|
+
{ name: "parent", type: parentType },
|
|
162
|
+
{ name: "label", type: "string" },
|
|
163
|
+
{ name: "issuedTime", type: "uint64" },
|
|
164
|
+
{ name: "context", type: "string" },
|
|
165
|
+
{ name: "secretHash", type: "bytes32" }
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
primaryType: "Delegation",
|
|
169
|
+
format: format
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
resolve({
|
|
173
|
+
label: label,
|
|
174
|
+
context: normalizedContext,
|
|
175
|
+
secret: derivedSecret,
|
|
176
|
+
statement: statement,
|
|
177
|
+
proof: proof
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
} catch (e) {
|
|
181
|
+
reject(e);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministically derive a signing keypair from a secret.
|
|
5
|
+
*
|
|
6
|
+
* SECURITY INVARIANTS:
|
|
7
|
+
* - This is the ONLY place secrets become private keys
|
|
8
|
+
* - No randomness
|
|
9
|
+
* - No storage
|
|
10
|
+
* - Deterministic & reproducible
|
|
11
|
+
*
|
|
12
|
+
* Supported formats:
|
|
13
|
+
* - "eip712" → secp256k1 (noble-curves). Public key and Ethereum
|
|
14
|
+
* address derived entirely from noble, no ethers dependency.
|
|
15
|
+
* - "es256" → NIST P-256 (noble-curves).
|
|
16
|
+
*
|
|
17
|
+
* Derivation method:
|
|
18
|
+
* - eip712: seed = keccak256("q.crypto.k256.private-key" || secret),
|
|
19
|
+
* scalar = seed mod curveOrder.
|
|
20
|
+
* - es256: scalar = HKDF-SHA256(secret, "q.crypto.p256.private-key")
|
|
21
|
+
* via Q.Data.derive().
|
|
22
|
+
*
|
|
23
|
+
* @static
|
|
24
|
+
* @method internalKeypair
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {Uint8Array} options.secret Raw secret material (32 bytes recommended).
|
|
28
|
+
* @param {String} [options.format="es256"] Signing format: "eip712" or "es256".
|
|
29
|
+
*
|
|
30
|
+
* @return {Q.Promise<Object>} Resolves to an object with the following properties:
|
|
31
|
+
*
|
|
32
|
+
* format {String} Normalized format: "eip712" or "es256".
|
|
33
|
+
* curve {String} Curve name: "secp256k1" or "p256".
|
|
34
|
+
* hashAlg {String} Hash algorithm: "keccak256" or "sha256".
|
|
35
|
+
* privateKey {Uint8Array} Raw private key scalar (32 bytes).
|
|
36
|
+
* Never log, store, or transmit this value.
|
|
37
|
+
* publicKey {Uint8Array} Raw uncompressed public key (65 bytes): 0x04 || X || Y.
|
|
38
|
+
*
|
|
39
|
+
* The following property is only present for "eip712":
|
|
40
|
+
* address {String} Ethereum address: lowercase "0x" + last 20 bytes of
|
|
41
|
+
* keccak256(publicKey[1..64]). Suitable for use
|
|
42
|
+
* as an EVM signer identity.
|
|
43
|
+
*
|
|
44
|
+
* @throws {Error} If secret is missing or not a Uint8Array.
|
|
45
|
+
* @throws {Error} If format is unsupported.
|
|
46
|
+
* @throws {Error} If derived private key scalar is zero (astronomically unlikely).
|
|
47
|
+
*/
|
|
48
|
+
return function Q_Crypto_internalKeypair(options) {
|
|
49
|
+
|
|
50
|
+
return new Q.Promise(async function (resolve, reject) {
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
|
|
54
|
+
if (!options || !(options.secret instanceof Uint8Array)) {
|
|
55
|
+
throw new Error("secret must be Uint8Array");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const secret = options.secret;
|
|
59
|
+
const format = options.format || "ES256";
|
|
60
|
+
|
|
61
|
+
// -------------------------------------------------
|
|
62
|
+
// EIP-712 / secp256k1 (noble-curves, no ethers)
|
|
63
|
+
// -------------------------------------------------
|
|
64
|
+
if (format === "EIP712") {
|
|
65
|
+
|
|
66
|
+
const [{ keccak_256 }, secp] = await Promise.all([
|
|
67
|
+
import(Q.url("{{Q}}/src/js/crypto/sha3.js")),
|
|
68
|
+
import(Q.url("{{Q}}/src/js/crypto/secp256k1.js"))
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// Domain-separated deterministic seed:
|
|
72
|
+
// keccak256("q.crypto.k256.private-key" || secret)
|
|
73
|
+
const info = new TextEncoder().encode("q.crypto.k256.private-key");
|
|
74
|
+
const material = new Uint8Array(info.length + secret.length);
|
|
75
|
+
material.set(info, 0);
|
|
76
|
+
material.set(secret, info.length);
|
|
77
|
+
|
|
78
|
+
const digest = keccak_256(material); // Uint8Array(32)
|
|
79
|
+
|
|
80
|
+
// Reduce mod curve order — matches PHP implementation
|
|
81
|
+
const n = secp.secp256k1.CURVE.n;
|
|
82
|
+
const k = bytesToBigInt(digest) % n;
|
|
83
|
+
|
|
84
|
+
if (k === 0n) {
|
|
85
|
+
throw new Error("Derived invalid secp256k1 scalar");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Private key as Uint8Array(32)
|
|
89
|
+
const privateKey = bigIntToBytes32(k);
|
|
90
|
+
|
|
91
|
+
// Uncompressed public key: 0x04 || X(32) || Y(32)
|
|
92
|
+
const publicKey = secp.secp256k1.getPublicKey(privateKey, false);
|
|
93
|
+
|
|
94
|
+
// Ethereum address: "0x" + last 20 bytes of keccak256(pubkey[1..])
|
|
95
|
+
const pubKeyBody = publicKey.slice(1); // drop 0x04 prefix
|
|
96
|
+
const addrHash = keccak_256(pubKeyBody); // Uint8Array(32)
|
|
97
|
+
const address = "0x" + Q.Data.toHex(addrHash.slice(12)); // last 20 bytes
|
|
98
|
+
|
|
99
|
+
resolve({
|
|
100
|
+
format: "eip712",
|
|
101
|
+
curve: "secp256k1",
|
|
102
|
+
hashAlg: "keccak256",
|
|
103
|
+
privateKey: privateKey, // Uint8Array(32)
|
|
104
|
+
publicKey: publicKey, // Uint8Array(65)
|
|
105
|
+
address: address // "0x..." lowercase
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// -------------------------------------------------
|
|
112
|
+
// es256 / P-256 (noble-curves)
|
|
113
|
+
// -------------------------------------------------
|
|
114
|
+
if (format === "ES256") {
|
|
115
|
+
|
|
116
|
+
const noble = await import(
|
|
117
|
+
Q.url("{{Q}}/src/js/crypto/nist.js")
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Deterministic scalar via HKDF-SHA256
|
|
121
|
+
const privateKey = await Q.Data.derive(
|
|
122
|
+
secret,
|
|
123
|
+
"q.crypto.p256.private-key",
|
|
124
|
+
{ size: 32 }
|
|
125
|
+
); // Uint8Array(32)
|
|
126
|
+
|
|
127
|
+
// Uncompressed public key: 0x04 || X(32) || Y(32)
|
|
128
|
+
const publicKey = noble.p256.getPublicKey(privateKey, false);
|
|
129
|
+
|
|
130
|
+
resolve({
|
|
131
|
+
format: "es256",
|
|
132
|
+
curve: "p256",
|
|
133
|
+
hashAlg: "sha256",
|
|
134
|
+
privateKey: privateKey, // Uint8Array(32)
|
|
135
|
+
publicKey: publicKey // Uint8Array(65)
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error("Unsupported format: " + format);
|
|
142
|
+
|
|
143
|
+
} catch (e) {
|
|
144
|
+
reject(e);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// -------------------------------------------------
|
|
150
|
+
// Module-private helpers
|
|
151
|
+
// -------------------------------------------------
|
|
152
|
+
|
|
153
|
+
function bytesToBigInt(bytes) {
|
|
154
|
+
let result = 0n;
|
|
155
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
156
|
+
result = (result << 8n) | BigInt(bytes[i]);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function bigIntToBytes32(n) {
|
|
162
|
+
const hex = n.toString(16).padStart(64, "0");
|
|
163
|
+
const out = new Uint8Array(32);
|
|
164
|
+
for (let i = 0; i < 32; i++) {
|
|
165
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Q.exports(function (Q) {
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sign a typed message using a deterministically derived keypair.
|
|
5
|
+
*
|
|
6
|
+
* This is the JS counterpart of Q_Crypto::sign() in PHP.
|
|
7
|
+
* It performs protocol-level typed signing (not raw message signing).
|
|
8
|
+
*
|
|
9
|
+
* Supported formats:
|
|
10
|
+
* - "ES256" → NIST P-256 + SHA-256 (Q-native)
|
|
11
|
+
* - "EIP712" → secp256k1 + keccak256 (Ethereum-style, full EIP-712 encoding)
|
|
12
|
+
*
|
|
13
|
+
* SECURITY PROPERTIES:
|
|
14
|
+
* - Secret is never stored
|
|
15
|
+
* - Keypair derivation is deterministic
|
|
16
|
+
* - Exactly one hash + one signature
|
|
17
|
+
* - Output is a verifiable proof object
|
|
18
|
+
*
|
|
19
|
+
* @static
|
|
20
|
+
* @method sign
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Uint8Array} options.secret Raw binary secret material.
|
|
24
|
+
* @param {Object} options.message Typed message payload.
|
|
25
|
+
* @param {Object} options.types Type definitions (EIP-712 style).
|
|
26
|
+
* @param {String} options.primaryType Root type name.
|
|
27
|
+
* @param {Object} [options.domain={}] Domain separator fields.
|
|
28
|
+
* @param {String} [options.format="ES256"] "ES256" or "EIP712".
|
|
29
|
+
*
|
|
30
|
+
* @return {Q.Promise<Object>} Resolves to an object with the following properties:
|
|
31
|
+
*
|
|
32
|
+
* format {String} Resolved format: "eip712" or "es256".
|
|
33
|
+
* curve {String} Elliptic curve: "secp256k1" or "p256".
|
|
34
|
+
* hashAlg {String} Hash algorithm: "keccak256" or "sha256".
|
|
35
|
+
* domain {Object} Domain separator used.
|
|
36
|
+
* primaryType {String} Root type name.
|
|
37
|
+
* digest {String} Hex-encoded message digest.
|
|
38
|
+
* signature {Uint8Array} Raw signature bytes.
|
|
39
|
+
* EIP712: 65 bytes r||s||v (v = 27 + recovery).
|
|
40
|
+
* ES256: DER-encoded ECDSA signature.
|
|
41
|
+
* signatureHex {String} Hex-encoded signature (no 0x prefix).
|
|
42
|
+
* publicKey {Uint8Array} Raw uncompressed public key (65 bytes).
|
|
43
|
+
*
|
|
44
|
+
* EIP712 only:
|
|
45
|
+
* address {String} Ethereum address of signer ("0x...").
|
|
46
|
+
*/
|
|
47
|
+
return function Q_Crypto_sign(options) {
|
|
48
|
+
|
|
49
|
+
return new Q.Promise(async function (resolve, reject) {
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
|
|
53
|
+
if (!options || !(options.secret instanceof Uint8Array)) {
|
|
54
|
+
throw new Error("secret must be Uint8Array");
|
|
55
|
+
}
|
|
56
|
+
if (!options.message || typeof options.message !== "object") {
|
|
57
|
+
throw new Error("message required");
|
|
58
|
+
}
|
|
59
|
+
if (!options.types || typeof options.types !== "object") {
|
|
60
|
+
throw new Error("types required");
|
|
61
|
+
}
|
|
62
|
+
if (typeof options.primaryType !== "string") {
|
|
63
|
+
throw new Error("primaryType required");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const domain = options.domain || {};
|
|
67
|
+
const format = options.format || "ES256";
|
|
68
|
+
|
|
69
|
+
// -------------------------------------------------
|
|
70
|
+
// Derive keypair ONCE
|
|
71
|
+
// -------------------------------------------------
|
|
72
|
+
const kp = await Q.Crypto.internalKeypair({
|
|
73
|
+
secret: options.secret,
|
|
74
|
+
format: format
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/* =================================================
|
|
78
|
+
* EIP712 (secp256k1 + keccak256)
|
|
79
|
+
* ================================================= */
|
|
80
|
+
if (format === "EIP712") {
|
|
81
|
+
|
|
82
|
+
const [{ hashTypedData }, secp] = await Promise.all([
|
|
83
|
+
import(Q.url("{{Q}}/src/js/crypto/eip712.js")),
|
|
84
|
+
import(Q.url("{{Q}}/src/js/crypto/secp256k1.js"))
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Single source of truth for EIP-712 digest —
|
|
88
|
+
// byte-identical to Q_Crypto_EIP712::hashTypedData() in PHP
|
|
89
|
+
const digestBytes = hashTypedData(
|
|
90
|
+
domain,
|
|
91
|
+
options.primaryType,
|
|
92
|
+
options.message,
|
|
93
|
+
options.types
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const sig = secp.secp256k1.sign(digestBytes, kp.privateKey, {
|
|
97
|
+
recovered: true,
|
|
98
|
+
der: false
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
let compact, recovery;
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(sig)) {
|
|
104
|
+
compact = sig[0];
|
|
105
|
+
recovery = sig[1];
|
|
106
|
+
} else {
|
|
107
|
+
compact = sig.signature;
|
|
108
|
+
recovery = sig.recovery;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!(compact instanceof Uint8Array)) {
|
|
112
|
+
compact = new Uint8Array(compact);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Ethereum-style: r||s||v (v = 27 + recovery)
|
|
116
|
+
const signature = new Uint8Array(65);
|
|
117
|
+
signature.set(compact, 0);
|
|
118
|
+
signature[64] = 27 + recovery;
|
|
119
|
+
|
|
120
|
+
resolve({
|
|
121
|
+
format: "eip712",
|
|
122
|
+
curve: "secp256k1",
|
|
123
|
+
hashAlg: "keccak256",
|
|
124
|
+
domain: domain,
|
|
125
|
+
primaryType: options.primaryType,
|
|
126
|
+
digest: Q.Data.toHex(digestBytes),
|
|
127
|
+
signature: signature,
|
|
128
|
+
signatureHex: Q.Data.toHex(signature),
|
|
129
|
+
publicKey: kp.publicKey,
|
|
130
|
+
address: kp.address
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* =================================================
|
|
137
|
+
* ES256 (P-256 + SHA-256)
|
|
138
|
+
* ================================================= */
|
|
139
|
+
|
|
140
|
+
// Canonical JSON payload — must match Q_Crypto::sign() in PHP exactly
|
|
141
|
+
const payload = {
|
|
142
|
+
domain: domain,
|
|
143
|
+
primaryType: options.primaryType,
|
|
144
|
+
types: options.types,
|
|
145
|
+
message: options.message
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const canonical = Q.serialize(payload);
|
|
149
|
+
const msgBytes = new TextEncoder().encode(canonical);
|
|
150
|
+
const digestBytes = await Q.Data.digest("SHA-256", msgBytes);
|
|
151
|
+
|
|
152
|
+
const noble = await import(
|
|
153
|
+
Q.url("{{Q}}/src/js/crypto/nist.js")
|
|
154
|
+
);
|
|
155
|
+
const { encodeEcdsaDer } = await import(
|
|
156
|
+
Q.url("{{Q}}/src/js/crypto/encoder.js")
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const sig = noble.p256
|
|
160
|
+
.sign(digestBytes, kp.privateKey)
|
|
161
|
+
.normalizeS();
|
|
162
|
+
|
|
163
|
+
const signatureDer = encodeEcdsaDer(
|
|
164
|
+
bigIntTo32Bytes(sig.r),
|
|
165
|
+
bigIntTo32Bytes(sig.s)
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
resolve({
|
|
169
|
+
format: "es256",
|
|
170
|
+
curve: "p256",
|
|
171
|
+
hashAlg: "sha256",
|
|
172
|
+
domain: domain,
|
|
173
|
+
primaryType: options.primaryType,
|
|
174
|
+
digest: Q.Data.toHex(digestBytes),
|
|
175
|
+
signature: signatureDer,
|
|
176
|
+
signatureHex: Q.Data.toHex(signatureDer),
|
|
177
|
+
publicKey: kp.publicKey
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
} catch (e) {
|
|
181
|
+
reject(e);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// -------------------------------------------------
|
|
187
|
+
// Module-private helper
|
|
188
|
+
// -------------------------------------------------
|
|
189
|
+
|
|
190
|
+
// BigInt → 32-byte Uint8Array (big-endian)
|
|
191
|
+
function bigIntTo32Bytes(n) {
|
|
192
|
+
const hex = n.toString(16).padStart(64, "0");
|
|
193
|
+
const out = new Uint8Array(32);
|
|
194
|
+
for (let i = 0; i < 32; i++) {
|
|
195
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
});
|