@swivy/core 0.0.0-stage → 1.0.1

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/README.md CHANGED
@@ -1,3 +1,18 @@
1
- # Temporary Holding Version
1
+ # @swivy/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Swivy shared types and sealed-export utilities. Testnet preview; production onboarding is gated on the Swop canary.
4
+
5
+ Public npm distribution is being prepared. Before first publication, use all SDK tarballs
6
+ from one operator-supplied release and verify its SHA256SUMS. After publication:
7
+
8
+ ```sh
9
+ npm install @swivy/core@next
10
+ ```
11
+
12
+ Use an environment app ID and publishable key for browser/mobile apps. Keep environment
13
+ secret keys on the backend and deployment bootstrap credentials with the deployment operator.
14
+ Choose email or SMS in your environment policy; SMS recovery alone does not enable SMS login.
15
+ Never log login codes, runtime sessions or signing material.
16
+
17
+ Browser apps currently need a same-origin reverse proxy for `/v1/runtime/*`; the API
18
+ does not advertise cross-origin browser access. Backend and native clients can use the API directly.
@@ -0,0 +1,25 @@
1
+ export declare function toBase64(bytes: Uint8Array): string;
2
+ export interface SealedExportEncryption {
3
+ info: string;
4
+ aad: string;
5
+ encapsulatedKey: string;
6
+ ciphertext: string;
7
+ aead?: string;
8
+ kem?: string;
9
+ kdf?: string;
10
+ }
11
+ /** A one-time X25519 key pair for receiving a sealed export. Send `publicKeyBase64`. */
12
+ export declare function generateExportRecipientKeyPair(): {
13
+ privateKey: Uint8Array;
14
+ publicKeyBase64: string;
15
+ };
16
+ export declare function openHpkeBase(params: {
17
+ recipientPrivateKey: Uint8Array;
18
+ encapsulatedKey: Uint8Array;
19
+ ciphertext: Uint8Array;
20
+ info: Uint8Array;
21
+ aad: Uint8Array;
22
+ aead?: "aes-128-gcm" | "aes-256-gcm";
23
+ }): Uint8Array;
24
+ /** Open the `encryption` object a sealed export returns. Throws if it was tampered with. */
25
+ export declare function openSealedExport(encryption: SealedExportEncryption, recipientPrivateKey: Uint8Array): string;
@@ -0,0 +1,89 @@
1
+ import { gcm } from "@noble/ciphers/aes";
2
+ import { x25519 } from "@noble/curves/ed25519";
3
+ import { extract as hkdfExtract, expand as hkdfExpand } from "@noble/hashes/hkdf";
4
+ import { sha256 } from "@noble/hashes/sha256";
5
+ // Client side of Swivy's sealed export: HPKE base mode (RFC 9180) with
6
+ // DHKEM(X25519, HKDF-SHA256), HKDF-SHA256 and AES-GCM. Pure JS, so it runs in browsers,
7
+ // React Native and Node. The signer seals with the same suite (secure-signer/src/hpke.ts).
8
+ const KEM_ID = 0x0020;
9
+ const KDF_ID = 0x0001;
10
+ const encoder = new TextEncoder();
11
+ function concat(...parts) {
12
+ const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
13
+ let offset = 0;
14
+ for (const part of parts) {
15
+ out.set(part, offset);
16
+ offset += part.length;
17
+ }
18
+ return out;
19
+ }
20
+ function i2osp(value, length) {
21
+ const out = new Uint8Array(length);
22
+ for (let index = length - 1; index >= 0; index -= 1) {
23
+ out[index] = value & 0xff;
24
+ value >>= 8;
25
+ }
26
+ return out;
27
+ }
28
+ function labeledExtract(suiteId, salt, label, ikm) {
29
+ return hkdfExtract(sha256, concat(encoder.encode("HPKE-v1"), suiteId, encoder.encode(label), ikm), salt);
30
+ }
31
+ function labeledExpand(suiteId, prk, label, info, length) {
32
+ return hkdfExpand(sha256, prk, concat(i2osp(length, 2), encoder.encode("HPKE-v1"), suiteId, encoder.encode(label), info), length);
33
+ }
34
+ function fromBase64(value) {
35
+ const binary = atob(value);
36
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
37
+ }
38
+ export function toBase64(bytes) {
39
+ let binary = "";
40
+ for (const byte of bytes) {
41
+ binary += String.fromCharCode(byte);
42
+ }
43
+ return btoa(binary);
44
+ }
45
+ /** A one-time X25519 key pair for receiving a sealed export. Send `publicKeyBase64`. */
46
+ export function generateExportRecipientKeyPair() {
47
+ const privateKey = x25519.utils.randomPrivateKey();
48
+ return { privateKey, publicKeyBase64: toBase64(x25519.getPublicKey(privateKey)) };
49
+ }
50
+ export function openHpkeBase(params) {
51
+ const aead = params.aead ?? "aes-256-gcm";
52
+ if (aead !== "aes-128-gcm" && aead !== "aes-256-gcm") {
53
+ throw new Error("unsupported_hpke_aead");
54
+ }
55
+ const aeadId = aead === "aes-128-gcm" ? 0x0001 : 0x0002;
56
+ const keyLength = aead === "aes-128-gcm" ? 16 : 32;
57
+ const recipientPublic = x25519.getPublicKey(params.recipientPrivateKey);
58
+ const dh = x25519.getSharedSecret(params.recipientPrivateKey, params.encapsulatedKey);
59
+ const kemSuite = concat(encoder.encode("KEM"), i2osp(KEM_ID, 2));
60
+ const eaePrk = labeledExtract(kemSuite, new Uint8Array(), "eae_prk", dh);
61
+ const sharedSecret = labeledExpand(kemSuite, eaePrk, "shared_secret", concat(params.encapsulatedKey, recipientPublic), 32);
62
+ const suiteId = concat(encoder.encode("HPKE"), i2osp(KEM_ID, 2), i2osp(KDF_ID, 2), i2osp(aeadId, 2));
63
+ const pskIdHash = labeledExtract(suiteId, new Uint8Array(), "psk_id_hash", new Uint8Array());
64
+ const infoHash = labeledExtract(suiteId, new Uint8Array(), "info_hash", params.info);
65
+ const context = concat(new Uint8Array([0]), pskIdHash, infoHash);
66
+ const secret = labeledExtract(suiteId, sharedSecret, "secret", new Uint8Array());
67
+ const key = labeledExpand(suiteId, secret, "key", context, keyLength);
68
+ const nonce = labeledExpand(suiteId, secret, "base_nonce", context, 12);
69
+ return gcm(key, nonce, params.aad).decrypt(params.ciphertext);
70
+ }
71
+ /** Open the `encryption` object a sealed export returns. Throws if it was tampered with. */
72
+ export function openSealedExport(encryption, recipientPrivateKey) {
73
+ // Older v1 clients omitted suite labels. Preserve that fixed-suite format,
74
+ // but never interpret an explicitly different or future suite as v1.
75
+ if (encryption.info !== "swivy-export-v1"
76
+ || (encryption.kem !== undefined && encryption.kem !== "DHKEM(X25519, HKDF-SHA256)")
77
+ || (encryption.kdf !== undefined && encryption.kdf !== "HKDF-SHA256")
78
+ || (encryption.aead !== undefined && encryption.aead !== "AES-256-GCM")) {
79
+ throw new Error("unsupported_sealed_export_suite");
80
+ }
81
+ const plaintext = openHpkeBase({
82
+ recipientPrivateKey,
83
+ encapsulatedKey: fromBase64(encryption.encapsulatedKey),
84
+ ciphertext: fromBase64(encryption.ciphertext),
85
+ info: encoder.encode(encryption.info),
86
+ aad: encoder.encode(encryption.aad),
87
+ });
88
+ return new TextDecoder().decode(plaintext);
89
+ }