@oxyhq/core 12.2.0 → 12.3.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/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/crypto/aead.js +79 -0
- package/dist/cjs/crypto/ecdh.js +53 -0
- package/dist/cjs/crypto/kdf.js +39 -0
- package/dist/cjs/crypto/keyManager.js +7 -0
- package/dist/cjs/crypto/recoveryPhrase.js +89 -1
- package/dist/cjs/i18n/locales/ar-SA.json +2 -0
- package/dist/cjs/i18n/locales/ca-ES.json +2 -0
- package/dist/cjs/i18n/locales/de-DE.json +2 -0
- package/dist/cjs/i18n/locales/en-US.json +7 -0
- package/dist/cjs/i18n/locales/es-ES.json +7 -0
- package/dist/cjs/i18n/locales/fr-FR.json +2 -0
- package/dist/cjs/i18n/locales/it-IT.json +2 -0
- package/dist/cjs/i18n/locales/ja-JP.json +2 -0
- package/dist/cjs/i18n/locales/ko-KR.json +2 -0
- package/dist/cjs/i18n/locales/locales/ar-SA.json +2 -0
- package/dist/cjs/i18n/locales/locales/ca-ES.json +2 -0
- package/dist/cjs/i18n/locales/locales/de-DE.json +2 -0
- package/dist/cjs/i18n/locales/locales/en-US.json +7 -0
- package/dist/cjs/i18n/locales/locales/es-ES.json +7 -0
- package/dist/cjs/i18n/locales/locales/fr-FR.json +2 -0
- package/dist/cjs/i18n/locales/locales/it-IT.json +2 -0
- package/dist/cjs/i18n/locales/locales/ja-JP.json +2 -0
- package/dist/cjs/i18n/locales/locales/ko-KR.json +2 -0
- package/dist/cjs/i18n/locales/locales/pt-PT.json +2 -0
- package/dist/cjs/i18n/locales/locales/zh-CN.json +2 -0
- package/dist/cjs/i18n/locales/pt-PT.json +2 -0
- package/dist/cjs/i18n/locales/zh-CN.json +2 -0
- package/dist/cjs/index.js +14 -4
- package/dist/cjs/mixins/OxyServices.identity.js +166 -0
- package/dist/cjs/mixins/OxyServices.identityBackup.js +161 -0
- package/dist/cjs/mixins/OxyServices.user.js +43 -0
- package/dist/cjs/mixins/index.js +4 -0
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/crypto/aead.js +74 -0
- package/dist/esm/crypto/ecdh.js +51 -0
- package/dist/esm/crypto/kdf.js +36 -0
- package/dist/esm/crypto/keyManager.js +7 -0
- package/dist/esm/crypto/recoveryPhrase.js +88 -0
- package/dist/esm/i18n/locales/ar-SA.json +2 -0
- package/dist/esm/i18n/locales/ca-ES.json +2 -0
- package/dist/esm/i18n/locales/de-DE.json +2 -0
- package/dist/esm/i18n/locales/en-US.json +7 -0
- package/dist/esm/i18n/locales/es-ES.json +7 -0
- package/dist/esm/i18n/locales/fr-FR.json +2 -0
- package/dist/esm/i18n/locales/it-IT.json +2 -0
- package/dist/esm/i18n/locales/ja-JP.json +2 -0
- package/dist/esm/i18n/locales/ko-KR.json +2 -0
- package/dist/esm/i18n/locales/locales/ar-SA.json +2 -0
- package/dist/esm/i18n/locales/locales/ca-ES.json +2 -0
- package/dist/esm/i18n/locales/locales/de-DE.json +2 -0
- package/dist/esm/i18n/locales/locales/en-US.json +7 -0
- package/dist/esm/i18n/locales/locales/es-ES.json +7 -0
- package/dist/esm/i18n/locales/locales/fr-FR.json +2 -0
- package/dist/esm/i18n/locales/locales/it-IT.json +2 -0
- package/dist/esm/i18n/locales/locales/ja-JP.json +2 -0
- package/dist/esm/i18n/locales/locales/ko-KR.json +2 -0
- package/dist/esm/i18n/locales/locales/pt-PT.json +2 -0
- package/dist/esm/i18n/locales/locales/zh-CN.json +2 -0
- package/dist/esm/i18n/locales/pt-PT.json +2 -0
- package/dist/esm/i18n/locales/zh-CN.json +2 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/mixins/OxyServices.identity.js +166 -0
- package/dist/esm/mixins/OxyServices.identityBackup.js +158 -0
- package/dist/esm/mixins/OxyServices.user.js +43 -0
- package/dist/esm/mixins/index.js +4 -0
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/crypto/aead.d.ts +56 -0
- package/dist/types/crypto/ecdh.d.ts +29 -0
- package/dist/types/crypto/kdf.d.ts +25 -0
- package/dist/types/crypto/keyManager.d.ts +5 -0
- package/dist/types/crypto/recoveryPhrase.d.ts +85 -0
- package/dist/types/index.d.ts +7 -3
- package/dist/types/mixins/OxyServices.identity.d.ts +95 -0
- package/dist/types/mixins/OxyServices.identityBackup.d.ts +129 -0
- package/dist/types/mixins/OxyServices.user.d.ts +38 -8
- package/dist/types/mixins/index.d.ts +2 -1
- package/package.json +4 -2
- package/src/crypto/__tests__/backupMaterial.test.ts +86 -0
- package/src/crypto/__tests__/cryptoPrimitives.test.ts +225 -0
- package/src/crypto/__tests__/keyManager.atomicity.test.ts +33 -0
- package/src/crypto/__tests__/recoveryPhrase.test.ts +61 -0
- package/src/crypto/aead.ts +97 -0
- package/src/crypto/ecdh.ts +60 -0
- package/src/crypto/kdf.ts +43 -0
- package/src/crypto/keyManager.ts +8 -0
- package/src/crypto/recoveryPhrase.ts +133 -0
- package/src/i18n/locales/ar-SA.json +2 -0
- package/src/i18n/locales/ca-ES.json +2 -0
- package/src/i18n/locales/de-DE.json +2 -0
- package/src/i18n/locales/en-US.json +7 -0
- package/src/i18n/locales/es-ES.json +7 -0
- package/src/i18n/locales/fr-FR.json +2 -0
- package/src/i18n/locales/it-IT.json +2 -0
- package/src/i18n/locales/ja-JP.json +2 -0
- package/src/i18n/locales/ko-KR.json +2 -0
- package/src/i18n/locales/pt-PT.json +2 -0
- package/src/i18n/locales/zh-CN.json +2 -0
- package/src/index.ts +16 -1
- package/src/mixins/OxyServices.identity.ts +250 -0
- package/src/mixins/OxyServices.identityBackup.ts +237 -0
- package/src/mixins/OxyServices.user.ts +82 -4
- package/src/mixins/__tests__/OxyServices.rotateKey.test.ts +277 -0
- package/src/mixins/__tests__/getFollowStatuses.test.ts +95 -0
- package/src/mixins/__tests__/identityBackup.test.ts +258 -0
- package/src/mixins/index.ts +5 -0
- package/src/types/elliptic.d.ts +10 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated Encryption with Associated Data (XChaCha20-Poly1305)
|
|
3
|
+
*
|
|
4
|
+
* Pure-JS/TS AEAD via `@noble/ciphers` — identical behaviour on web, Node, and
|
|
5
|
+
* React Native with zero WebCrypto / native-module dependency. This replaces
|
|
6
|
+
* the `crypto.subtle`-based path (unreliable on React Native) for the Commons
|
|
7
|
+
* encrypted backup and device-to-device transfer flows.
|
|
8
|
+
*
|
|
9
|
+
* XChaCha20-Poly1305 is chosen over AES-GCM specifically for its 24-byte
|
|
10
|
+
* (192-bit) random nonce: the nonce space is large enough that random nonces
|
|
11
|
+
* never collide in practice, so callers do not need to maintain a per-key
|
|
12
|
+
* counter. The 16-byte Poly1305 tag is appended to the ciphertext by the
|
|
13
|
+
* underlying library and validated on decrypt.
|
|
14
|
+
*
|
|
15
|
+
* The optional Associated Data (AAD) is authenticated but NOT encrypted: it
|
|
16
|
+
* binds the ciphertext to its context (e.g. a backup version, a device id, a
|
|
17
|
+
* DID). Decryption fails if the key, nonce, ciphertext, OR aad differ from
|
|
18
|
+
* those used at encryption time.
|
|
19
|
+
*
|
|
20
|
+
* ESM/CJS safe: static `import` only, no `require()`.
|
|
21
|
+
*/
|
|
22
|
+
// Loading the polyfill guarantees `globalThis.crypto.getRandomValues` exists on
|
|
23
|
+
// every platform (native crypto on web/Node; expo-crypto-backed shim on RN).
|
|
24
|
+
import './polyfill.js';
|
|
25
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha';
|
|
26
|
+
/** Key length for XChaCha20-Poly1305, in bytes (256-bit). */
|
|
27
|
+
export const AEAD_KEY_LENGTH = 32;
|
|
28
|
+
/** Nonce length for XChaCha20-Poly1305, in bytes (192-bit). */
|
|
29
|
+
export const AEAD_NONCE_LENGTH = 24;
|
|
30
|
+
function assertKey(key) {
|
|
31
|
+
if (key.length !== AEAD_KEY_LENGTH) {
|
|
32
|
+
throw new Error(`AEAD key must be ${AEAD_KEY_LENGTH} bytes, got ${key.length}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Generate a fresh 24-byte random nonce via the platform CSPRNG. */
|
|
36
|
+
function randomNonce() {
|
|
37
|
+
const nonce = new Uint8Array(AEAD_NONCE_LENGTH);
|
|
38
|
+
globalThis.crypto.getRandomValues(nonce);
|
|
39
|
+
return nonce;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Encrypt `plaintext` under `key` with a fresh random nonce, authenticating the
|
|
43
|
+
* optional `aad`.
|
|
44
|
+
*
|
|
45
|
+
* @param key 32-byte symmetric key (e.g. from `hkdfSha256`).
|
|
46
|
+
* @param plaintext Bytes to encrypt.
|
|
47
|
+
* @param aad Optional associated data authenticated but not encrypted.
|
|
48
|
+
* @returns `{ nonce, ciphertext }` — both are required to decrypt.
|
|
49
|
+
*/
|
|
50
|
+
export function encryptAead(key, plaintext, aad) {
|
|
51
|
+
assertKey(key);
|
|
52
|
+
const nonce = randomNonce();
|
|
53
|
+
const ciphertext = xchacha20poly1305(key, nonce, aad).encrypt(plaintext);
|
|
54
|
+
return { nonce, ciphertext };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Decrypt and authenticate `ciphertext` produced by {@link encryptAead}.
|
|
58
|
+
*
|
|
59
|
+
* Throws if the key, nonce, ciphertext, or aad differ from those used at
|
|
60
|
+
* encryption time (tamper detection), or if the tag is invalid.
|
|
61
|
+
*
|
|
62
|
+
* @param key 32-byte symmetric key.
|
|
63
|
+
* @param nonce The 24-byte nonce returned by `encryptAead`.
|
|
64
|
+
* @param ciphertext Ciphertext with the appended Poly1305 tag.
|
|
65
|
+
* @param aad The same associated data supplied at encryption time.
|
|
66
|
+
* @returns The recovered plaintext bytes.
|
|
67
|
+
*/
|
|
68
|
+
export function decryptAead(key, nonce, ciphertext, aad) {
|
|
69
|
+
assertKey(key);
|
|
70
|
+
if (nonce.length !== AEAD_NONCE_LENGTH) {
|
|
71
|
+
throw new Error(`AEAD nonce must be ${AEAD_NONCE_LENGTH} bytes, got ${nonce.length}`);
|
|
72
|
+
}
|
|
73
|
+
return xchacha20poly1305(key, nonce, aad).decrypt(ciphertext);
|
|
74
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH shared-secret derivation (secp256k1)
|
|
3
|
+
*
|
|
4
|
+
* Derives a raw 32-byte ECDH shared secret from a local private key and a
|
|
5
|
+
* remote public key, using the SAME `elliptic` `EC('secp256k1')` primitive the
|
|
6
|
+
* rest of core's identity layer uses (`keyManager.ts`). This is the key-exchange
|
|
7
|
+
* step for the Commons device-to-device transfer flow: each side computes the
|
|
8
|
+
* same shared secret, which is then run through `hkdfSha256` to derive the
|
|
9
|
+
* symmetric key handed to `encryptAead` / `decryptAead`.
|
|
10
|
+
*
|
|
11
|
+
* The returned value is the raw x-coordinate of the ECDH point, big-endian,
|
|
12
|
+
* zero-padded to 32 bytes. It is NOT itself a symmetric key — always pass it
|
|
13
|
+
* through a KDF (HKDF) with a context-binding `info` before use.
|
|
14
|
+
*
|
|
15
|
+
* ESM/CJS safe: static `import` only, no `require()`.
|
|
16
|
+
*/
|
|
17
|
+
import _cjs_elliptic from 'elliptic';
|
|
18
|
+
const { ec: EC } = _cjs_elliptic;
|
|
19
|
+
const ec = new EC('secp256k1');
|
|
20
|
+
/** Lowercase and left-pad a private-key hex string to canonical 64-char form. */
|
|
21
|
+
function canonicalPrivateKey(key) {
|
|
22
|
+
return key.toLowerCase().padStart(64, '0');
|
|
23
|
+
}
|
|
24
|
+
const HEX_RE = /^[0-9a-fA-F]+$/;
|
|
25
|
+
/**
|
|
26
|
+
* Compute the ECDH shared secret between a local private key and a remote
|
|
27
|
+
* public key on secp256k1.
|
|
28
|
+
*
|
|
29
|
+
* Symmetric by construction:
|
|
30
|
+
* `deriveSharedSecret(privA, pubB) === deriveSharedSecret(privB, pubA)`.
|
|
31
|
+
*
|
|
32
|
+
* @param privateKeyHex Local private key, hex (up to 64 chars; canonicalized).
|
|
33
|
+
* @param otherPublicKeyHex Remote public key, hex — compressed (`02`/`03` + 32
|
|
34
|
+
* bytes) or uncompressed (`04` + 64 bytes).
|
|
35
|
+
* @returns The 32-byte big-endian shared secret.
|
|
36
|
+
*/
|
|
37
|
+
export function deriveSharedSecret(privateKeyHex, otherPublicKeyHex) {
|
|
38
|
+
if (typeof privateKeyHex !== 'string' || !HEX_RE.test(privateKeyHex)) {
|
|
39
|
+
throw new Error('deriveSharedSecret: privateKeyHex must be a hex string');
|
|
40
|
+
}
|
|
41
|
+
if (typeof otherPublicKeyHex !== 'string' || !HEX_RE.test(otherPublicKeyHex)) {
|
|
42
|
+
throw new Error('deriveSharedSecret: otherPublicKeyHex must be a hex string');
|
|
43
|
+
}
|
|
44
|
+
const keyPair = ec.keyFromPrivate(canonicalPrivateKey(privateKeyHex));
|
|
45
|
+
const otherKey = ec.keyFromPublic(otherPublicKeyHex, 'hex');
|
|
46
|
+
// `derive` returns a BN (the shared point's x-coordinate). Serialize it
|
|
47
|
+
// big-endian, fixed 32 bytes, so both sides agree byte-for-byte regardless of
|
|
48
|
+
// any leading-zero stripping.
|
|
49
|
+
const sharedBytes = keyPair.derive(otherKey.getPublic()).toArray('be', 32);
|
|
50
|
+
return Uint8Array.from(sharedBytes);
|
|
51
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key Derivation Function (HKDF-SHA256)
|
|
3
|
+
*
|
|
4
|
+
* Pure-JS/TS HKDF via `@noble/hashes` — identical behaviour on web, Node, and
|
|
5
|
+
* React Native with zero WebCrypto / native-module dependency. Used to derive
|
|
6
|
+
* fixed-length symmetric keys from higher-entropy input keying material (an
|
|
7
|
+
* ECDH shared secret, a recovery-phrase seed, etc.) for the Commons encrypted
|
|
8
|
+
* backup and device-to-device transfer flows.
|
|
9
|
+
*
|
|
10
|
+
* ESM/CJS safe: static `import` only, no `require()`.
|
|
11
|
+
*/
|
|
12
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
13
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
14
|
+
/**
|
|
15
|
+
* Derive `length` bytes of keying material from `ikm` using HKDF-SHA256
|
|
16
|
+
* (RFC 5869 — extract-then-expand).
|
|
17
|
+
*
|
|
18
|
+
* @param ikm Input keying material (the raw secret; NOT necessarily uniform).
|
|
19
|
+
* @param salt Non-secret random salt. An empty array is treated by HKDF as a
|
|
20
|
+
* zero-filled salt of the hash length — pass a real salt whenever
|
|
21
|
+
* one is available so derivations for different contexts diverge.
|
|
22
|
+
* @param info Context/application-binding string ("what is this key for").
|
|
23
|
+
* Distinct `info` values yield independent keys from the same ikm.
|
|
24
|
+
* @param length Number of output bytes. Must be in (0, 255 * 32].
|
|
25
|
+
* @returns Exactly `length` bytes of derived keying material.
|
|
26
|
+
*/
|
|
27
|
+
export function hkdfSha256(ikm, salt, info, length) {
|
|
28
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
29
|
+
throw new Error('hkdfSha256: length must be a positive integer');
|
|
30
|
+
}
|
|
31
|
+
// HKDF-Expand is defined for at most 255 * HashLen bytes of output.
|
|
32
|
+
if (length > 255 * 32) {
|
|
33
|
+
throw new Error('hkdfSha256: length must not exceed 8160 bytes (255 * 32)');
|
|
34
|
+
}
|
|
35
|
+
return hkdf(sha256, ikm, salt, info, length);
|
|
36
|
+
}
|
|
@@ -1291,6 +1291,13 @@ export class KeyManager {
|
|
|
1291
1291
|
const keyPair = ec.keyFromPrivate(KeyManager.canonicalPrivateKey(privateKey));
|
|
1292
1292
|
return keyPair.getPublic('hex');
|
|
1293
1293
|
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Normalize a public key to uncompressed, lowercased hex. Used when building
|
|
1296
|
+
* signed rotation payloads so legacy compressed/cased encodings still verify.
|
|
1297
|
+
*/
|
|
1298
|
+
static canonicalPublicKey(publicKey) {
|
|
1299
|
+
return ec.keyFromPublic(publicKey, 'hex').getPublic(false, 'hex').toLowerCase();
|
|
1300
|
+
}
|
|
1294
1301
|
/**
|
|
1295
1302
|
* Validate that a string is a valid public key
|
|
1296
1303
|
*
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import * as bip39 from 'bip39';
|
|
10
10
|
import { KeyManager } from './keyManager.js';
|
|
11
|
+
import { hkdfSha256 } from './kdf.js';
|
|
11
12
|
/**
|
|
12
13
|
* Convert Uint8Array or array-like to hexadecimal string
|
|
13
14
|
* Works in both Node.js and React Native without depending on Buffer
|
|
@@ -19,6 +20,22 @@ function toHex(data) {
|
|
|
19
20
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
20
21
|
.join('');
|
|
21
22
|
}
|
|
23
|
+
/** UTF-8 encode an ASCII label to bytes (for HKDF salt/info). */
|
|
24
|
+
function utf8(label) {
|
|
25
|
+
return new TextEncoder().encode(label);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* HKDF context tag for the encrypted-backup key schedule (b3 Feature 1). Used as
|
|
29
|
+
* the HKDF `salt`; distinct from any other Oxy key-derivation salt so the backup
|
|
30
|
+
* key schedule is independent. Versioned so a future scheme change is a new tag.
|
|
31
|
+
*/
|
|
32
|
+
export const BACKUP_KDF_SALT = 'oxy-identity-backup-v1';
|
|
33
|
+
/** HKDF `info` label that derives the symmetric AEAD key from the seed. */
|
|
34
|
+
export const BACKUP_KDF_ENCRYPTION_INFO = 'oxy-backup-encryption-key';
|
|
35
|
+
/** HKDF `info` label that derives the (server-hashed) backup locator from the seed. */
|
|
36
|
+
export const BACKUP_KDF_LOOKUP_INFO = 'oxy-backup-lookup-id';
|
|
37
|
+
/** Byte length of both the derived backup key and the derived lookup id (256-bit). */
|
|
38
|
+
export const BACKUP_MATERIAL_LENGTH = 32;
|
|
22
39
|
export class RecoveryPhraseService {
|
|
23
40
|
/**
|
|
24
41
|
* Generate a new identity with a recovery phrase.
|
|
@@ -69,6 +86,77 @@ export class RecoveryPhraseService {
|
|
|
69
86
|
publicKey,
|
|
70
87
|
};
|
|
71
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Derive a brand-new identity + recovery phrase WITHOUT persisting anything.
|
|
91
|
+
*
|
|
92
|
+
* Pure: same derivation as {@link generateIdentityWithRecovery} (128-bit
|
|
93
|
+
* mnemonic → seed → first 32 bytes as the secp256k1 private key) but it stops
|
|
94
|
+
* BEFORE `KeyManager.importKeyPair`, so no on-device identity is touched. The
|
|
95
|
+
* caller decides if/when to commit the material (e.g. only after a server
|
|
96
|
+
* confirms a key rotation). Works on web too — it never reads or writes secure
|
|
97
|
+
* storage.
|
|
98
|
+
*
|
|
99
|
+
* The 12-word `phrase` MUST be shown to the user before the identity is
|
|
100
|
+
* committed anywhere — if it is lost the account becomes unrecoverable.
|
|
101
|
+
*/
|
|
102
|
+
static async derivePendingIdentity() {
|
|
103
|
+
const mnemonic = bip39.generateMnemonic(128);
|
|
104
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
105
|
+
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
106
|
+
const privateKey = toHex(seedSlice);
|
|
107
|
+
const publicKey = KeyManager.derivePublicKey(privateKey);
|
|
108
|
+
return {
|
|
109
|
+
phrase: mnemonic,
|
|
110
|
+
words: mnemonic.split(' '),
|
|
111
|
+
privateKey,
|
|
112
|
+
publicKey,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Derive the private key from a recovery phrase WITHOUT storing it.
|
|
117
|
+
*
|
|
118
|
+
* The private-key counterpart of {@link derivePublicKeyFromPhrase}. Used to
|
|
119
|
+
* re-derive a key in memory (e.g. to sign a rotation proof with the current
|
|
120
|
+
* key when the device has no SecureStore copy). Never persists — the returned
|
|
121
|
+
* material lives only in the caller's memory.
|
|
122
|
+
*/
|
|
123
|
+
static async derivePrivateKeyFromPhrase(phrase) {
|
|
124
|
+
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
125
|
+
if (!bip39.validateMnemonic(normalizedPhrase)) {
|
|
126
|
+
throw new Error('Invalid recovery phrase');
|
|
127
|
+
}
|
|
128
|
+
const seed = await bip39.mnemonicToSeed(normalizedPhrase);
|
|
129
|
+
const seedSlice = seed.subarray ? seed.subarray(0, 32) : seed.slice(0, 32);
|
|
130
|
+
return toHex(seedSlice);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Derive the encrypted-backup key material from a recovery phrase (b3 Feature
|
|
134
|
+
* 1). PURE and additive — it does NOT touch the frozen phrase→privateKey
|
|
135
|
+
* derivation ({@link derivePrivateKeyFromPhrase} slices the first 32 seed
|
|
136
|
+
* bytes) and never reads or writes secure storage.
|
|
137
|
+
*
|
|
138
|
+
* Both outputs are derived from the FULL 64-byte BIP-39 seed via HKDF-SHA256
|
|
139
|
+
* with domain-separated `info` labels, so the domain separation is real: a
|
|
140
|
+
* device compromise that leaks only the raw 32-byte private key can compute
|
|
141
|
+
* NEITHER the backup key nor the lookup id (both need the whole seed). Locating
|
|
142
|
+
* AND decrypting a backup therefore requires the recovery phrase.
|
|
143
|
+
*
|
|
144
|
+
* @param phrase - The BIP-39 recovery phrase (validated + normalized here).
|
|
145
|
+
* @returns `{ backupKey, lookupId }` — the AEAD key (kept local) and the hex
|
|
146
|
+
* locator (uploaded; server stores only its hash).
|
|
147
|
+
* @throws if the phrase is not a valid BIP-39 mnemonic.
|
|
148
|
+
*/
|
|
149
|
+
static async deriveBackupMaterial(phrase) {
|
|
150
|
+
const normalizedPhrase = phrase.trim().toLowerCase();
|
|
151
|
+
if (!bip39.validateMnemonic(normalizedPhrase)) {
|
|
152
|
+
throw new Error('Invalid recovery phrase. Please check the words and try again.');
|
|
153
|
+
}
|
|
154
|
+
const seed = await bip39.mnemonicToSeed(normalizedPhrase);
|
|
155
|
+
const salt = utf8(BACKUP_KDF_SALT);
|
|
156
|
+
const backupKey = hkdfSha256(seed, salt, utf8(BACKUP_KDF_ENCRYPTION_INFO), BACKUP_MATERIAL_LENGTH);
|
|
157
|
+
const lookupId = toHex(hkdfSha256(seed, salt, utf8(BACKUP_KDF_LOOKUP_INFO), BACKUP_MATERIAL_LENGTH));
|
|
158
|
+
return { backupKey, lookupId };
|
|
159
|
+
}
|
|
72
160
|
/**
|
|
73
161
|
* Restore an identity from a recovery phrase.
|
|
74
162
|
*
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "أنشئ حسابك",
|
|
54
|
+
"subtitle": "هوية واحدة لكل النظام البيئي.",
|
|
53
55
|
"commonsHeadline": "أنشئ هويتك في Commons",
|
|
54
56
|
"commonsExplainer": "Commons هي هوية Oxy الخاصة بك بحفظ ذاتي كامل — تطبيق واحد يعمل في كل مكان.",
|
|
55
57
|
"createInCommons": "أنشئ هويتك في Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea el teu compte",
|
|
54
|
+
"subtitle": "Una identitat per a tot l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la teva identitat a Commons",
|
|
54
56
|
"commonsExplainer": "Commons és la teva identitat d'Oxy amb autocustòdia completa: una app que funciona a tot arreu.",
|
|
55
57
|
"createInCommons": "Crea la teva identitat a Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Erstellen Sie Ihr Konto",
|
|
54
|
+
"subtitle": "Eine Identität für das gesamte Ökosystem.",
|
|
53
55
|
"commonsHeadline": "Erstellen Sie Ihre Identität in Commons",
|
|
54
56
|
"commonsExplainer": "Commons ist Ihre vollständig selbstverwaltete Oxy-Identität – eine App, überall einsetzbar.",
|
|
55
57
|
"createInCommons": "Erstellen Sie Ihre Identität in Commons",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Create your account",
|
|
61
|
+
"subtitle": "One identity for the whole ecosystem.",
|
|
60
62
|
"commonsHeadline": "Create your identity in Commons",
|
|
61
63
|
"commonsExplainer": "Commons is your full self-custody Oxy identity — one app, works everywhere.",
|
|
62
64
|
"createInCommons": "Create your identity in Commons",
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
"backToSignInLink": "Already have an account? Sign in",
|
|
71
73
|
"hubExplainer": "Create your Oxy ID in a secure window, then come right back.",
|
|
72
74
|
"continueInWindow": "Continue in a new window",
|
|
75
|
+
"toasts": {
|
|
76
|
+
"passkeyCreateFailed": "We couldn't create your account. Please try again."
|
|
77
|
+
},
|
|
73
78
|
"welcome": {
|
|
74
79
|
"title": "Welcome to Oxy!",
|
|
75
80
|
"subtitle": "Let's create your account in just a few steps",
|
|
@@ -372,6 +377,8 @@
|
|
|
372
377
|
"toasts": {
|
|
373
378
|
"switchSuccess": "Account switched successfully!",
|
|
374
379
|
"switchFailed": "There was a problem switching accounts. Please try again.",
|
|
380
|
+
"signInFailed": "Sign-in failed. Please try again.",
|
|
381
|
+
"passkeySignInFailed": "Passkey sign-in failed. Please try again.",
|
|
375
382
|
"removeSuccess": "Account removed successfully!",
|
|
376
383
|
"removeFailed": "There was a problem removing the account. Please try again.",
|
|
377
384
|
"signOutAllSuccess": "All accounts signed out successfully!",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Crea tu cuenta",
|
|
61
|
+
"subtitle": "Una identidad para todo el ecosistema.",
|
|
60
62
|
"commonsHeadline": "Crea tu identidad en Commons",
|
|
61
63
|
"commonsExplainer": "Commons es tu identidad de Oxy con autocustodia completa: una app que funciona en todas partes.",
|
|
62
64
|
"createInCommons": "Crea tu identidad en Commons",
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
"backToSignInLink": "¿Ya tienes cuenta? Inicia sesión",
|
|
71
73
|
"hubExplainer": "Crea tu Oxy ID en una ventana segura y vuelve enseguida.",
|
|
72
74
|
"continueInWindow": "Continuar en una ventana nueva",
|
|
75
|
+
"toasts": {
|
|
76
|
+
"passkeyCreateFailed": "No pudimos crear tu cuenta. Inténtalo de nuevo."
|
|
77
|
+
},
|
|
73
78
|
"welcome": {
|
|
74
79
|
"title": "¡Te damos la bienvenida a Oxy!",
|
|
75
80
|
"subtitle": "Crea tu cuenta en pocos pasos",
|
|
@@ -570,6 +575,8 @@
|
|
|
570
575
|
"toasts": {
|
|
571
576
|
"switchSuccess": "¡Cuenta cambiada correctamente!",
|
|
572
577
|
"switchFailed": "Hubo un problema al cambiar de cuenta. Inténtalo de nuevo.",
|
|
578
|
+
"signInFailed": "No se pudo iniciar sesión. Inténtalo de nuevo.",
|
|
579
|
+
"passkeySignInFailed": "No se pudo iniciar sesión con la llave de acceso. Inténtalo de nuevo.",
|
|
573
580
|
"removeSuccess": "¡Cuenta eliminada correctamente!",
|
|
574
581
|
"removeFailed": "Hubo un problema al eliminar la cuenta. Inténtalo de nuevo.",
|
|
575
582
|
"signOutAllSuccess": "¡Todas las cuentas cerradas correctamente!",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Créez votre compte",
|
|
54
|
+
"subtitle": "Une seule identité pour tout l'écosystème.",
|
|
53
55
|
"commonsHeadline": "Créez votre identité dans Commons",
|
|
54
56
|
"commonsExplainer": "Commons est votre identité Oxy en autoconservation complète : une seule app, partout.",
|
|
55
57
|
"createInCommons": "Créez votre identité dans Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea il tuo account",
|
|
54
|
+
"subtitle": "Un'unica identità per tutto l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la tua identità in Commons",
|
|
54
56
|
"commonsExplainer": "Commons è la tua identità Oxy in piena autocustodia: un'unica app, ovunque.",
|
|
55
57
|
"createInCommons": "Crea la tua identità in Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "アカウントを作成",
|
|
54
|
+
"subtitle": "エコシステム全体で使える1つのアイデンティティ。",
|
|
53
55
|
"commonsHeadline": "Commonsであなたのアイデンティティを作成",
|
|
54
56
|
"commonsExplainer": "Commonsは完全に自己管理できるあなたのOxy IDです。1つのアプリでどこでも使えます。",
|
|
55
57
|
"createInCommons": "Commonsであなたのアイデンティティを作成",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "계정 만들기",
|
|
54
|
+
"subtitle": "생태계 전체를 위한 하나의 아이덴티티.",
|
|
53
55
|
"commonsHeadline": "Commons에서 나의 아이덴티티 만들기",
|
|
54
56
|
"commonsExplainer": "Commons는 완전한 자기 보관 방식의 Oxy 아이덴티티입니다. 하나의 앱으로 어디서나 사용하세요.",
|
|
55
57
|
"createInCommons": "Commons에서 나의 아이덴티티 만들기",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "أنشئ حسابك",
|
|
54
|
+
"subtitle": "هوية واحدة لكل النظام البيئي.",
|
|
53
55
|
"commonsHeadline": "أنشئ هويتك في Commons",
|
|
54
56
|
"commonsExplainer": "Commons هي هوية Oxy الخاصة بك بحفظ ذاتي كامل — تطبيق واحد يعمل في كل مكان.",
|
|
55
57
|
"createInCommons": "أنشئ هويتك في Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea el teu compte",
|
|
54
|
+
"subtitle": "Una identitat per a tot l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la teva identitat a Commons",
|
|
54
56
|
"commonsExplainer": "Commons és la teva identitat d'Oxy amb autocustòdia completa: una app que funciona a tot arreu.",
|
|
55
57
|
"createInCommons": "Crea la teva identitat a Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Erstellen Sie Ihr Konto",
|
|
54
|
+
"subtitle": "Eine Identität für das gesamte Ökosystem.",
|
|
53
55
|
"commonsHeadline": "Erstellen Sie Ihre Identität in Commons",
|
|
54
56
|
"commonsExplainer": "Commons ist Ihre vollständig selbstverwaltete Oxy-Identität – eine App, überall einsetzbar.",
|
|
55
57
|
"createInCommons": "Erstellen Sie Ihre Identität in Commons",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Create your account",
|
|
61
|
+
"subtitle": "One identity for the whole ecosystem.",
|
|
60
62
|
"commonsHeadline": "Create your identity in Commons",
|
|
61
63
|
"commonsExplainer": "Commons is your full self-custody Oxy identity — one app, works everywhere.",
|
|
62
64
|
"createInCommons": "Create your identity in Commons",
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
"backToSignInLink": "Already have an account? Sign in",
|
|
71
73
|
"hubExplainer": "Create your Oxy ID in a secure window, then come right back.",
|
|
72
74
|
"continueInWindow": "Continue in a new window",
|
|
75
|
+
"toasts": {
|
|
76
|
+
"passkeyCreateFailed": "We couldn't create your account. Please try again."
|
|
77
|
+
},
|
|
73
78
|
"welcome": {
|
|
74
79
|
"title": "Welcome to Oxy!",
|
|
75
80
|
"subtitle": "Let's create your account in just a few steps",
|
|
@@ -372,6 +377,8 @@
|
|
|
372
377
|
"toasts": {
|
|
373
378
|
"switchSuccess": "Account switched successfully!",
|
|
374
379
|
"switchFailed": "There was a problem switching accounts. Please try again.",
|
|
380
|
+
"signInFailed": "Sign-in failed. Please try again.",
|
|
381
|
+
"passkeySignInFailed": "Passkey sign-in failed. Please try again.",
|
|
375
382
|
"removeSuccess": "Account removed successfully!",
|
|
376
383
|
"removeFailed": "There was a problem removing the account. Please try again.",
|
|
377
384
|
"signOutAllSuccess": "All accounts signed out successfully!",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
}
|
|
58
58
|
},
|
|
59
59
|
"signup": {
|
|
60
|
+
"title": "Crea tu cuenta",
|
|
61
|
+
"subtitle": "Una identidad para todo el ecosistema.",
|
|
60
62
|
"commonsHeadline": "Crea tu identidad en Commons",
|
|
61
63
|
"commonsExplainer": "Commons es tu identidad de Oxy con autocustodia completa: una app que funciona en todas partes.",
|
|
62
64
|
"createInCommons": "Crea tu identidad en Commons",
|
|
@@ -70,6 +72,9 @@
|
|
|
70
72
|
"backToSignInLink": "¿Ya tienes cuenta? Inicia sesión",
|
|
71
73
|
"hubExplainer": "Crea tu Oxy ID en una ventana segura y vuelve enseguida.",
|
|
72
74
|
"continueInWindow": "Continuar en una ventana nueva",
|
|
75
|
+
"toasts": {
|
|
76
|
+
"passkeyCreateFailed": "No pudimos crear tu cuenta. Inténtalo de nuevo."
|
|
77
|
+
},
|
|
73
78
|
"welcome": {
|
|
74
79
|
"title": "¡Te damos la bienvenida a Oxy!",
|
|
75
80
|
"subtitle": "Crea tu cuenta en pocos pasos",
|
|
@@ -570,6 +575,8 @@
|
|
|
570
575
|
"toasts": {
|
|
571
576
|
"switchSuccess": "¡Cuenta cambiada correctamente!",
|
|
572
577
|
"switchFailed": "Hubo un problema al cambiar de cuenta. Inténtalo de nuevo.",
|
|
578
|
+
"signInFailed": "No se pudo iniciar sesión. Inténtalo de nuevo.",
|
|
579
|
+
"passkeySignInFailed": "No se pudo iniciar sesión con la llave de acceso. Inténtalo de nuevo.",
|
|
573
580
|
"removeSuccess": "¡Cuenta eliminada correctamente!",
|
|
574
581
|
"removeFailed": "Hubo un problema al eliminar la cuenta. Inténtalo de nuevo.",
|
|
575
582
|
"signOutAllSuccess": "¡Todas las cuentas cerradas correctamente!",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Créez votre compte",
|
|
54
|
+
"subtitle": "Une seule identité pour tout l'écosystème.",
|
|
53
55
|
"commonsHeadline": "Créez votre identité dans Commons",
|
|
54
56
|
"commonsExplainer": "Commons est votre identité Oxy en autoconservation complète : une seule app, partout.",
|
|
55
57
|
"createInCommons": "Créez votre identité dans Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crea il tuo account",
|
|
54
|
+
"subtitle": "Un'unica identità per tutto l'ecosistema.",
|
|
53
55
|
"commonsHeadline": "Crea la tua identità in Commons",
|
|
54
56
|
"commonsExplainer": "Commons è la tua identità Oxy in piena autocustodia: un'unica app, ovunque.",
|
|
55
57
|
"createInCommons": "Crea la tua identità in Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "アカウントを作成",
|
|
54
|
+
"subtitle": "エコシステム全体で使える1つのアイデンティティ。",
|
|
53
55
|
"commonsHeadline": "Commonsであなたのアイデンティティを作成",
|
|
54
56
|
"commonsExplainer": "Commonsは完全に自己管理できるあなたのOxy IDです。1つのアプリでどこでも使えます。",
|
|
55
57
|
"createInCommons": "Commonsであなたのアイデンティティを作成",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "계정 만들기",
|
|
54
|
+
"subtitle": "생태계 전체를 위한 하나의 아이덴티티.",
|
|
53
55
|
"commonsHeadline": "Commons에서 나의 아이덴티티 만들기",
|
|
54
56
|
"commonsExplainer": "Commons는 완전한 자기 보관 방식의 Oxy 아이덴티티입니다. 하나의 앱으로 어디서나 사용하세요.",
|
|
55
57
|
"createInCommons": "Commons에서 나의 아이덴티티 만들기",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crie a sua conta",
|
|
54
|
+
"subtitle": "Uma identidade para todo o ecossistema.",
|
|
53
55
|
"commonsHeadline": "Crie a sua identidade no Commons",
|
|
54
56
|
"commonsExplainer": "O Commons é a sua identidade Oxy com autocustódia total: uma app que funciona em todo o lado.",
|
|
55
57
|
"createInCommons": "Crie a sua identidade no Commons",
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"signup": {
|
|
53
|
+
"title": "Crie a sua conta",
|
|
54
|
+
"subtitle": "Uma identidade para todo o ecossistema.",
|
|
53
55
|
"commonsHeadline": "Crie a sua identidade no Commons",
|
|
54
56
|
"commonsExplainer": "O Commons é a sua identidade Oxy com autocustódia total: uma app que funciona em todo o lado.",
|
|
55
57
|
"createInCommons": "Crie a sua identidade no Commons",
|
package/dist/esm/index.js
CHANGED
|
@@ -65,6 +65,10 @@ export { mergeSessions, normalizeAndSortSessions, sessionsArraysEqual, } from '.
|
|
|
65
65
|
export { KeyManager, IdentityAlreadyExistsError, IdentityPersistError, } from './crypto/keyManager.js';
|
|
66
66
|
export { SignatureService } from './crypto/signatureService.js';
|
|
67
67
|
export { RecoveryPhraseService } from './crypto/recoveryPhrase.js';
|
|
68
|
+
// Low-level crypto primitives (b3 Phase 0 — encrypted backup + device transfer)
|
|
69
|
+
export { hkdfSha256 } from './crypto/kdf.js';
|
|
70
|
+
export { encryptAead, decryptAead, AEAD_KEY_LENGTH, AEAD_NONCE_LENGTH, } from './crypto/aead.js';
|
|
71
|
+
export { deriveSharedSecret } from './crypto/ecdh.js';
|
|
68
72
|
// ---------------------------------------------------------------------------
|
|
69
73
|
// Devices
|
|
70
74
|
// ---------------------------------------------------------------------------
|