@graphprotocol/hypergraph 0.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/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/connect/auth-storage.d.ts.map +1 -0
- package/dist/connect/create-app-identity.d.ts.map +1 -0
- package/dist/connect/create-auth-url.d.ts.map +1 -0
- package/dist/connect/create-auth-url.js +35 -0
- package/dist/connect/create-auth-url.js.map +1 -0
- package/dist/connect/create-callback-params.d.ts.map +1 -0
- package/dist/connect/create-callback-params.js +17 -0
- package/dist/connect/create-callback-params.js.map +1 -0
- package/dist/connect/create-identity-keys.d.ts.map +1 -0
- package/dist/connect/identity-encryption.d.ts.map +1 -0
- package/dist/connect/index.d.ts.map +1 -0
- package/dist/connect/login.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.d.ts.map +1 -0
- package/dist/connect/parse-callback-params.js +63 -0
- package/dist/connect/parse-callback-params.js.map +1 -0
- package/dist/connect/prove-ownership.d.ts.map +1 -0
- package/dist/connect/types.d.ts +57 -0
- package/dist/connect/types.d.ts.map +1 -0
- package/dist/connect/types.js +24 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/entity/create.d.ts.map +1 -0
- package/dist/entity/decodedEntitiesCache.d.ts.map +1 -0
- package/dist/entity/delete.d.ts.map +1 -0
- package/dist/entity/entity.d.ts.map +1 -0
- package/dist/entity/entityRelationParentsMap.d.ts.map +1 -0
- package/dist/entity/findMany.d.ts.map +1 -0
- package/dist/entity/findMany.js +436 -0
- package/dist/entity/findMany.js.map +1 -0
- package/dist/entity/findOne.d.ts.map +1 -0
- package/dist/entity/getEntityRelations.d.ts.map +1 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/relationParentsMap.d.ts.map +1 -0
- package/dist/entity/removeRelation.d.ts.map +1 -0
- package/dist/entity/types.d.ts +79 -0
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +2 -0
- package/dist/entity/types.js.map +1 -0
- package/dist/entity/update.d.ts.map +1 -0
- package/dist/identity/auth-storage.d.ts.map +1 -0
- package/dist/identity/get-verified-identity.d.ts.map +1 -0
- package/dist/identity/identity-encryption.d.ts.map +1 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/logout.d.ts.map +1 -0
- package/dist/identity/prove-ownership.d.ts.map +1 -0
- package/dist/inboxes/create-inbox.d.ts.map +1 -0
- package/dist/inboxes/get-list-inboxes.d.ts.map +1 -0
- package/dist/inboxes/index.d.ts.map +1 -0
- package/dist/inboxes/merge-messages.d.ts.map +1 -0
- package/dist/inboxes/message-encryption.d.ts.map +1 -0
- package/dist/inboxes/message-validation.d.ts.map +1 -0
- package/dist/inboxes/prepare-message.d.ts +31 -0
- package/dist/inboxes/prepare-message.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-creator.d.ts.map +1 -0
- package/dist/inboxes/recover-inbox-message-signer.d.ts.map +1 -0
- package/dist/inboxes/send-message.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/signed-update-message.d.ts.map +1 -0
- package/dist/messages/types.d.ts.map +1 -0
- package/dist/space-events/accept-invitation.d.ts.map +1 -0
- package/dist/space-events/apply-event.d.ts.map +1 -0
- package/dist/space-events/create-inbox.d.ts.map +1 -0
- package/dist/space-events/create-invitation.d.ts.map +1 -0
- package/dist/space-events/create-space.d.ts.map +1 -0
- package/dist/space-events/delete-space.d.ts.map +1 -0
- package/dist/space-events/hash-event.d.ts.map +1 -0
- package/dist/space-events/index.d.ts.map +1 -0
- package/dist/space-info/decrypt-space-info.d.ts.map +1 -0
- package/dist/space-info/encrypt-and-sign-space-info.d.ts.map +1 -0
- package/dist/space-info/index.d.ts.map +1 -0
- package/dist/store-connect.d.ts.map +1 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/type/type.d.ts.map +1 -0
- package/dist/utils/automergeId.d.ts +9 -0
- package/dist/utils/automergeId.d.ts.map +1 -0
- package/dist/utils/automergeId.js +17 -0
- package/dist/utils/automergeId.js.map +1 -0
- package/dist/utils/generateId.d.ts +15 -0
- package/dist/utils/generateId.d.ts.map +1 -0
- package/dist/utils/generateId.js +18 -0
- package/dist/utils/generateId.js.map +1 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +35 -0
- package/src/connect/auth-storage.ts +67 -0
- package/src/connect/create-app-identity.ts +16 -0
- package/src/connect/create-auth-url.ts +42 -0
- package/src/connect/create-callback-params.ts +30 -0
- package/src/connect/create-identity-keys.ts +20 -0
- package/src/connect/identity-encryption.ts +232 -0
- package/src/connect/index.ts +10 -0
- package/src/connect/login.ts +114 -0
- package/src/connect/parse-auth-params.ts +37 -0
- package/src/connect/parse-callback-params.ts +67 -0
- package/src/connect/prove-ownership.ts +58 -0
- package/src/connect/types.ts +67 -0
- package/src/entity/create.ts +58 -0
- package/src/entity/decodedEntitiesCache.ts +38 -0
- package/src/entity/delete.ts +52 -0
- package/src/entity/entity.ts +26 -0
- package/src/entity/entityRelationParentsMap.ts +6 -0
- package/src/entity/findMany.ts +506 -0
- package/src/entity/findOne.ts +34 -0
- package/src/entity/getEntityRelations.ts +45 -0
- package/src/entity/hasValidTypesProperty.ts +8 -0
- package/src/entity/index.ts +8 -0
- package/src/entity/relationParentsMap.ts +6 -0
- package/src/entity/removeRelation.ts +21 -0
- package/src/entity/test.ts +0 -0
- package/src/entity/types.ts +100 -0
- package/src/entity/update.ts +58 -0
- package/src/entity/variant-schema.ts +677 -0
- package/src/identity/auth-storage.ts +57 -0
- package/src/identity/get-verified-identity.ts +53 -0
- package/src/identity/identity-encryption.ts +140 -0
- package/src/identity/index.ts +6 -0
- package/src/identity/logout.ts +8 -0
- package/src/identity/prove-ownership.ts +58 -0
- package/src/identity/types.ts +44 -0
- package/src/inboxes/create-inbox.ts +102 -0
- package/src/inboxes/get-list-inboxes.ts +52 -0
- package/src/inboxes/index.ts +10 -0
- package/src/inboxes/merge-messages.ts +28 -0
- package/src/inboxes/message-encryption.ts +35 -0
- package/src/inboxes/message-validation.ts +66 -0
- package/src/inboxes/prepare-message.ts +85 -0
- package/src/inboxes/recover-inbox-creator.ts +29 -0
- package/src/inboxes/recover-inbox-message-signer.ts +42 -0
- package/src/inboxes/send-message.ts +75 -0
- package/src/inboxes/types.ts +9 -0
- package/src/index.ts +13 -0
- package/src/key/create-key.ts +27 -0
- package/src/key/decrypt-key.ts +19 -0
- package/src/key/encrypt-key.ts +27 -0
- package/src/key/index.ts +4 -0
- package/src/key/key-box.ts +31 -0
- package/src/messages/decrypt-message.ts +13 -0
- package/src/messages/encrypt-message.ts +14 -0
- package/src/messages/index.ts +5 -0
- package/src/messages/serialize.ts +24 -0
- package/src/messages/signed-update-message.ts +84 -0
- package/src/messages/types.ts +506 -0
- package/src/space-events/accept-invitation.ts +36 -0
- package/src/space-events/apply-event.ts +150 -0
- package/src/space-events/create-inbox.ts +56 -0
- package/src/space-events/create-invitation.ts +41 -0
- package/src/space-events/create-space.ts +35 -0
- package/src/space-events/delete-space.ts +36 -0
- package/src/space-events/hash-event.ts +10 -0
- package/src/space-events/index.ts +8 -0
- package/src/space-events/types.ts +137 -0
- package/src/space-info/decrypt-space-info.ts +22 -0
- package/src/space-info/encrypt-and-sign-space-info.ts +50 -0
- package/src/space-info/index.ts +3 -0
- package/src/space-info/types.ts +7 -0
- package/src/store-connect.ts +504 -0
- package/src/store.ts +493 -0
- package/src/type/type.ts +25 -0
- package/src/types.ts +47 -0
- package/src/utils/assertExhaustive.ts +3 -0
- package/src/utils/automergeId.ts +18 -0
- package/src/utils/base58.ts +74 -0
- package/src/utils/generateId.ts +18 -0
- package/src/utils/hexBytesAddressUtils.ts +25 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/internal/base58Utils.ts +47 -0
- package/src/utils/internal/deep-merge.ts +38 -0
- package/src/utils/isRelationField.ts +9 -0
- package/src/utils/jsc.ts +94 -0
- package/src/utils/stringToUint8Array.ts +9 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PrivateAppIdentity } from '../connect/types.js';
|
|
2
|
+
import type { Storage } from './types.js';
|
|
3
|
+
|
|
4
|
+
export const storeIdentity = (storage: Storage, identity: PrivateAppIdentity) => {
|
|
5
|
+
storage.setItem('hypergraph:app-identity-address', identity.address);
|
|
6
|
+
storage.setItem('hypergraph:app-identity-address-private-key', identity.addressPrivateKey);
|
|
7
|
+
storage.setItem('hypergraph:signature-public-key', identity.signaturePublicKey);
|
|
8
|
+
storage.setItem('hypergraph:signature-private-key', identity.signaturePrivateKey);
|
|
9
|
+
storage.setItem('hypergraph:encryption-public-key', identity.encryptionPublicKey);
|
|
10
|
+
storage.setItem('hypergraph:encryption-private-key', identity.encryptionPrivateKey);
|
|
11
|
+
storage.setItem('hypergraph:session-token', identity.sessionToken);
|
|
12
|
+
storage.setItem('hypergraph:session-token-expires', identity.sessionTokenExpires.toISOString());
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const loadIdentity = (storage: Storage): PrivateAppIdentity | null => {
|
|
16
|
+
const address = storage.getItem('hypergraph:app-identity-address');
|
|
17
|
+
const addressPrivateKey = storage.getItem('hypergraph:app-identity-address-private-key');
|
|
18
|
+
const signaturePublicKey = storage.getItem('hypergraph:signature-public-key');
|
|
19
|
+
const signaturePrivateKey = storage.getItem('hypergraph:signature-private-key');
|
|
20
|
+
const encryptionPublicKey = storage.getItem('hypergraph:encryption-public-key');
|
|
21
|
+
const encryptionPrivateKey = storage.getItem('hypergraph:encryption-private-key');
|
|
22
|
+
const sessionToken = storage.getItem('hypergraph:session-token');
|
|
23
|
+
const sessionTokenExpires = storage.getItem('hypergraph:session-token-expires');
|
|
24
|
+
if (
|
|
25
|
+
!address ||
|
|
26
|
+
!addressPrivateKey ||
|
|
27
|
+
!signaturePublicKey ||
|
|
28
|
+
!signaturePrivateKey ||
|
|
29
|
+
!encryptionPublicKey ||
|
|
30
|
+
!encryptionPrivateKey ||
|
|
31
|
+
!sessionToken ||
|
|
32
|
+
!sessionTokenExpires
|
|
33
|
+
) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
address,
|
|
38
|
+
addressPrivateKey,
|
|
39
|
+
signaturePublicKey,
|
|
40
|
+
signaturePrivateKey,
|
|
41
|
+
encryptionPublicKey,
|
|
42
|
+
encryptionPrivateKey,
|
|
43
|
+
sessionToken,
|
|
44
|
+
sessionTokenExpires: new Date(sessionTokenExpires),
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const wipeIdentity = (storage: Storage) => {
|
|
49
|
+
storage.removeItem('hypergraph:app-identity-address');
|
|
50
|
+
storage.removeItem('hypergraph:app-identity-address-private-key');
|
|
51
|
+
storage.removeItem('hypergraph:signature-public-key');
|
|
52
|
+
storage.removeItem('hypergraph:signature-private-key');
|
|
53
|
+
storage.removeItem('hypergraph:encryption-public-key');
|
|
54
|
+
storage.removeItem('hypergraph:encryption-private-key');
|
|
55
|
+
storage.removeItem('hypergraph:session-token');
|
|
56
|
+
storage.removeItem('hypergraph:session-token-expires');
|
|
57
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as Schema from 'effect/Schema';
|
|
2
|
+
import * as Messages from '../messages/index.js';
|
|
3
|
+
import { store } from '../store.js';
|
|
4
|
+
import { verifyIdentityOwnership } from './prove-ownership.js';
|
|
5
|
+
|
|
6
|
+
export const getVerifiedIdentity = async (
|
|
7
|
+
accountAddress: string,
|
|
8
|
+
syncServerUri: string,
|
|
9
|
+
): Promise<{
|
|
10
|
+
accountAddress: string;
|
|
11
|
+
encryptionPublicKey: string;
|
|
12
|
+
signaturePublicKey: string;
|
|
13
|
+
}> => {
|
|
14
|
+
const storeState = store.getSnapshot();
|
|
15
|
+
const identity = storeState.context.identities[accountAddress];
|
|
16
|
+
if (identity) {
|
|
17
|
+
return {
|
|
18
|
+
accountAddress,
|
|
19
|
+
encryptionPublicKey: identity.encryptionPublicKey,
|
|
20
|
+
signaturePublicKey: identity.signaturePublicKey,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const res = await fetch(`${syncServerUri}/identity?accountAddress=${accountAddress}`);
|
|
24
|
+
if (res.status !== 200) {
|
|
25
|
+
throw new Error('Failed to fetch identity');
|
|
26
|
+
}
|
|
27
|
+
const resDecoded = Schema.decodeUnknownSync(Messages.ResponseIdentity)(await res.json());
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
!(await verifyIdentityOwnership(
|
|
31
|
+
resDecoded.accountAddress,
|
|
32
|
+
resDecoded.signaturePublicKey,
|
|
33
|
+
resDecoded.accountProof,
|
|
34
|
+
resDecoded.keyProof,
|
|
35
|
+
))
|
|
36
|
+
) {
|
|
37
|
+
throw new Error('Invalid identity');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
store.send({
|
|
41
|
+
type: 'addVerifiedIdentity',
|
|
42
|
+
accountAddress: resDecoded.accountAddress,
|
|
43
|
+
encryptionPublicKey: resDecoded.encryptionPublicKey,
|
|
44
|
+
signaturePublicKey: resDecoded.signaturePublicKey,
|
|
45
|
+
accountProof: resDecoded.accountProof,
|
|
46
|
+
keyProof: resDecoded.keyProof,
|
|
47
|
+
});
|
|
48
|
+
return {
|
|
49
|
+
accountAddress: resDecoded.accountAddress,
|
|
50
|
+
encryptionPublicKey: resDecoded.encryptionPublicKey,
|
|
51
|
+
signaturePublicKey: resDecoded.signaturePublicKey,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { gcm } from '@noble/ciphers/aes';
|
|
2
|
+
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
3
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
4
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
5
|
+
import type { Hex } from 'viem';
|
|
6
|
+
import { verifyMessage } from 'viem';
|
|
7
|
+
|
|
8
|
+
import { bytesToHex, canonicalize, hexToBytes } from '../utils/index.js';
|
|
9
|
+
import type { IdentityKeys, Signer } from './types.js';
|
|
10
|
+
|
|
11
|
+
// Adapted from the XMTP approach to encrypt keys
|
|
12
|
+
// See: https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L79-L116
|
|
13
|
+
// (We reimplement their encrypt/decrypt functions using noble).
|
|
14
|
+
|
|
15
|
+
const hkdfDeriveKey = (secret: Uint8Array, salt: Uint8Array): Uint8Array => {
|
|
16
|
+
return hkdf(sha256, secret, salt, '', 32);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// This implements the same encryption as https://github.com/xmtp/xmtp-js/blob/336471de4ea95416ad0f4f9850d3f12bb0a13f1e/sdks/js-sdk/src/encryption/encryption.ts#L18
|
|
20
|
+
// But using @noble/ciphers instead of the WebCrypto API.
|
|
21
|
+
// The XMTP code was audited by Certik: https://skynet.certik.com/projects/xmtp
|
|
22
|
+
//
|
|
23
|
+
// Worth noting that GCM nonce collision would break the encryption,
|
|
24
|
+
// and 12 bytes is not a lot. So this function should not be used to encrypt
|
|
25
|
+
// a large number of messages with the same secret. In our case it should be okay
|
|
26
|
+
// as each secret is only used to encrypt a single identity. If we need
|
|
27
|
+
// something more secure for a larger number of messages we should use a
|
|
28
|
+
// different encryption scheme, e.g. XAES-256-GCM, see https://words.filippo.io/dispatches/xaes-256-gcm/
|
|
29
|
+
const encrypt = (msg: Uint8Array, secret: Uint8Array): string => {
|
|
30
|
+
const hkdfSalt = randomBytes(32);
|
|
31
|
+
const gcmNonce = randomBytes(12);
|
|
32
|
+
const derivedKey = hkdfDeriveKey(secret, hkdfSalt);
|
|
33
|
+
|
|
34
|
+
const aes = gcm(derivedKey, gcmNonce);
|
|
35
|
+
|
|
36
|
+
const ciphertext = aes.encrypt(msg);
|
|
37
|
+
|
|
38
|
+
// TODO: Use Effect Schema and better serialization?
|
|
39
|
+
const ciphertextJson = canonicalize({
|
|
40
|
+
aes256GcmHkdfSha256: {
|
|
41
|
+
payload: bytesToHex(ciphertext),
|
|
42
|
+
hkdfSalt: bytesToHex(hkdfSalt),
|
|
43
|
+
gcmNonce: bytesToHex(gcmNonce),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return bytesToHex(new TextEncoder().encode(ciphertextJson));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// This implements the same decryption as https://github.com/xmtp/xmtp-js/blob/336471de4ea95416ad0f4f9850d3f12bb0a13f1e/sdks/js-sdk/src/encryption/encryption.ts#L41
|
|
50
|
+
// But using @noble/ciphers instead of the WebCrypto API
|
|
51
|
+
// The XMTP code was audited by Certik: https://skynet.certik.com/projects/xmtp
|
|
52
|
+
const decrypt = (ciphertext: string, secret: Uint8Array): Uint8Array => {
|
|
53
|
+
const ciphertextJson = new TextDecoder().decode(hexToBytes(ciphertext));
|
|
54
|
+
const { aes256GcmHkdfSha256 } = JSON.parse(ciphertextJson);
|
|
55
|
+
const hkdfSalt = hexToBytes(aes256GcmHkdfSha256.hkdfSalt);
|
|
56
|
+
const gcmNonce = hexToBytes(aes256GcmHkdfSha256.gcmNonce);
|
|
57
|
+
const derivedKey = hkdfDeriveKey(secret, hkdfSalt);
|
|
58
|
+
|
|
59
|
+
const aes = gcm(derivedKey, gcmNonce);
|
|
60
|
+
|
|
61
|
+
return aes.decrypt(hexToBytes(aes256GcmHkdfSha256.payload));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const signatureMessage = (nonce: Uint8Array): string => {
|
|
65
|
+
return `The Graph: sign to encrypt/decrypt identity keys.\nNonce: ${bytesToHex(nonce)}\n`;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const encryptIdentity = async (
|
|
69
|
+
signer: Signer,
|
|
70
|
+
accountAddress: string,
|
|
71
|
+
keys: IdentityKeys,
|
|
72
|
+
): Promise<{ ciphertext: string; nonce: string }> => {
|
|
73
|
+
const nonce = randomBytes(32);
|
|
74
|
+
const message = signatureMessage(nonce);
|
|
75
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
76
|
+
|
|
77
|
+
// Check that the signature is valid
|
|
78
|
+
const valid = await verifyMessage({
|
|
79
|
+
address: accountAddress as Hex,
|
|
80
|
+
message,
|
|
81
|
+
signature,
|
|
82
|
+
});
|
|
83
|
+
if (!valid) {
|
|
84
|
+
throw new Error('Invalid signature');
|
|
85
|
+
}
|
|
86
|
+
const secretKey = hexToBytes(signature);
|
|
87
|
+
// We use a simple plaintext encoding:
|
|
88
|
+
// Hex keys separated by newlines
|
|
89
|
+
const keysTxt = [
|
|
90
|
+
keys.encryptionPublicKey,
|
|
91
|
+
keys.encryptionPrivateKey,
|
|
92
|
+
keys.signaturePublicKey,
|
|
93
|
+
keys.signaturePrivateKey,
|
|
94
|
+
].join('\n');
|
|
95
|
+
const keysMsg = new TextEncoder().encode(keysTxt);
|
|
96
|
+
const ciphertext = encrypt(keysMsg, secretKey);
|
|
97
|
+
return { ciphertext, nonce: bytesToHex(nonce) };
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const decryptIdentity = async (
|
|
101
|
+
signer: Signer,
|
|
102
|
+
accountAddress: string,
|
|
103
|
+
ciphertext: string,
|
|
104
|
+
nonce: string,
|
|
105
|
+
): Promise<IdentityKeys> => {
|
|
106
|
+
const message = signatureMessage(hexToBytes(nonce));
|
|
107
|
+
const signature = (await signer.signMessage(message)) as Hex;
|
|
108
|
+
|
|
109
|
+
// Check that the signature is valid
|
|
110
|
+
const valid = await verifyMessage({
|
|
111
|
+
address: accountAddress as Hex,
|
|
112
|
+
message,
|
|
113
|
+
signature,
|
|
114
|
+
});
|
|
115
|
+
if (!valid) {
|
|
116
|
+
throw new Error('Invalid signature');
|
|
117
|
+
}
|
|
118
|
+
const secretKey = hexToBytes(signature);
|
|
119
|
+
let keysMsg: Uint8Array;
|
|
120
|
+
try {
|
|
121
|
+
keysMsg = await decrypt(ciphertext, secretKey);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// See https://github.com/xmtp/xmtp-js/blob/8d6e5a65813902926baac8150a648587acbaad92/sdks/js-sdk/src/keystore/providers/NetworkKeyManager.ts#L142-L146
|
|
124
|
+
if (secretKey.length !== 65) {
|
|
125
|
+
throw new Error('Expected 65 bytes before trying a different recovery byte');
|
|
126
|
+
}
|
|
127
|
+
// Try the other version of recovery byte, either +27 or -27
|
|
128
|
+
const lastByte = secretKey[secretKey.length - 1];
|
|
129
|
+
let newSecret = secretKey.slice(0, secretKey.length - 1);
|
|
130
|
+
if (lastByte < 27) {
|
|
131
|
+
newSecret = new Uint8Array([...newSecret, lastByte + 27]);
|
|
132
|
+
} else {
|
|
133
|
+
newSecret = new Uint8Array([...newSecret, lastByte - 27]);
|
|
134
|
+
}
|
|
135
|
+
keysMsg = await decrypt(ciphertext, newSecret);
|
|
136
|
+
}
|
|
137
|
+
const keysTxt = new TextDecoder().decode(keysMsg);
|
|
138
|
+
const [encryptionPublicKey, encryptionPrivateKey, signaturePublicKey, signaturePrivateKey] = keysTxt.split('\n');
|
|
139
|
+
return { encryptionPublicKey, encryptionPrivateKey, signaturePublicKey, signaturePrivateKey };
|
|
140
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type Hex, verifyMessage } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
|
|
4
|
+
import { publicKeyToAddress } from '../utils/index.js';
|
|
5
|
+
import type { IdentityKeys, Signer } from './types.js';
|
|
6
|
+
|
|
7
|
+
export const getAccountProofMessage = (accountAddress: string, publicKey: string): string => {
|
|
8
|
+
return `This message proves I am the owner of the account ${accountAddress} and the public key ${publicKey}`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getKeyProofMessage = (accountAddress: string, publicKey: string): string => {
|
|
12
|
+
return `The public key ${publicKey} is owned by the account ${accountAddress}`;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const proveIdentityOwnership = async (
|
|
16
|
+
signer: Signer,
|
|
17
|
+
accountAddress: string,
|
|
18
|
+
keys: IdentityKeys,
|
|
19
|
+
): Promise<{ accountProof: string; keyProof: string }> => {
|
|
20
|
+
const publicKey = keys.signaturePublicKey;
|
|
21
|
+
const accountProofMessage = getAccountProofMessage(accountAddress, publicKey);
|
|
22
|
+
const keyProofMessage = getKeyProofMessage(accountAddress, publicKey);
|
|
23
|
+
const accountProof = await signer.signMessage(accountProofMessage);
|
|
24
|
+
const account = privateKeyToAccount(keys.signaturePrivateKey as Hex);
|
|
25
|
+
const keyProof = await account.signMessage({ message: keyProofMessage });
|
|
26
|
+
return { accountProof, keyProof };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const verifyIdentityOwnership = async (
|
|
30
|
+
accountAddress: string,
|
|
31
|
+
publicKey: string,
|
|
32
|
+
accountProof: string,
|
|
33
|
+
keyProof: string,
|
|
34
|
+
): Promise<boolean> => {
|
|
35
|
+
const accountProofMessage = getAccountProofMessage(accountAddress, publicKey);
|
|
36
|
+
const keyProofMessage = getKeyProofMessage(accountAddress, publicKey);
|
|
37
|
+
const validAccountProof = await verifyMessage({
|
|
38
|
+
address: accountAddress as Hex,
|
|
39
|
+
message: accountProofMessage,
|
|
40
|
+
signature: accountProof as Hex,
|
|
41
|
+
});
|
|
42
|
+
if (!validAccountProof) {
|
|
43
|
+
console.log('Invalid account proof');
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const keyAddress = publicKeyToAddress(publicKey) as Hex;
|
|
48
|
+
const validKeyProof = await verifyMessage({
|
|
49
|
+
address: keyAddress,
|
|
50
|
+
message: keyProofMessage,
|
|
51
|
+
signature: keyProof as Hex,
|
|
52
|
+
});
|
|
53
|
+
if (!validKeyProof) {
|
|
54
|
+
console.log('Invalid key proof');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
|
|
3
|
+
export type Storage = {
|
|
4
|
+
getItem: (key: string) => string | null;
|
|
5
|
+
setItem: (key: string, value: string) => void;
|
|
6
|
+
removeItem: (key: string) => void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type SignMessage = (message: string) => Promise<string> | string;
|
|
10
|
+
export type GetAddress = () => Promise<string> | string;
|
|
11
|
+
export type Signer = {
|
|
12
|
+
getAddress: GetAddress;
|
|
13
|
+
signMessage: SignMessage;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type IdentityKeys = {
|
|
17
|
+
encryptionPublicKey: string;
|
|
18
|
+
encryptionPrivateKey: string;
|
|
19
|
+
signaturePublicKey: string;
|
|
20
|
+
signaturePrivateKey: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const KeysSchema = Schema.Struct({
|
|
24
|
+
encryptionPublicKey: Schema.String,
|
|
25
|
+
encryptionPrivateKey: Schema.String,
|
|
26
|
+
signaturePublicKey: Schema.String,
|
|
27
|
+
signaturePrivateKey: Schema.String,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
export type KeysSchema = Schema.Schema.Type<typeof KeysSchema>;
|
|
31
|
+
|
|
32
|
+
export type Identity = IdentityKeys & {
|
|
33
|
+
accountAddress: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type PublicIdentity = {
|
|
37
|
+
accountAddress: string;
|
|
38
|
+
encryptionPublicKey: string;
|
|
39
|
+
signaturePublicKey: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export class InvalidIdentityError {
|
|
43
|
+
readonly _tag = 'InvalidIdentityError';
|
|
44
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
2
|
+
import { randomBytes } from '@noble/hashes/utils';
|
|
3
|
+
import { cryptoBoxKeyPair } from '@serenity-kit/noble-sodium';
|
|
4
|
+
import { Effect } from 'effect';
|
|
5
|
+
import * as Messages from '../messages/index.js';
|
|
6
|
+
import * as SpaceEvents from '../space-events/index.js';
|
|
7
|
+
import { bytesToHex, canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js';
|
|
8
|
+
import type * as Inboxes from './types.js';
|
|
9
|
+
|
|
10
|
+
type CreateAccountInboxParams = {
|
|
11
|
+
accountAddress: string;
|
|
12
|
+
isPublic: boolean;
|
|
13
|
+
authPolicy: Inboxes.InboxSenderAuthPolicy;
|
|
14
|
+
encryptionPublicKey: string;
|
|
15
|
+
signaturePrivateKey: string;
|
|
16
|
+
|
|
17
|
+
// TODO: add optional schema
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type CreateSpaceInboxParams = {
|
|
21
|
+
author: SpaceEvents.Author;
|
|
22
|
+
spaceId: string;
|
|
23
|
+
isPublic: boolean;
|
|
24
|
+
authPolicy: Inboxes.InboxSenderAuthPolicy;
|
|
25
|
+
spaceSecretKey: string;
|
|
26
|
+
previousEventHash: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// The caller should have already verified that the accountAddress, signaturePrivateKey and encryptionPublicKey belong to the same account
|
|
30
|
+
export function createAccountInboxCreationMessage({
|
|
31
|
+
accountAddress,
|
|
32
|
+
isPublic,
|
|
33
|
+
authPolicy,
|
|
34
|
+
encryptionPublicKey,
|
|
35
|
+
signaturePrivateKey,
|
|
36
|
+
}: CreateAccountInboxParams): Messages.RequestCreateAccountInbox {
|
|
37
|
+
// Generate a 32 byte random inbox id
|
|
38
|
+
const inboxId = bytesToHex(randomBytes(32));
|
|
39
|
+
|
|
40
|
+
// This message can prove to anyone wanting to send a message to the inbox that it is indeed from the account
|
|
41
|
+
// and that the public key belongs to the account
|
|
42
|
+
const messageToSign = stringToUint8Array(
|
|
43
|
+
canonicalize({
|
|
44
|
+
accountAddress,
|
|
45
|
+
inboxId,
|
|
46
|
+
encryptionPublicKey,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const signature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true });
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
type: 'create-account-inbox',
|
|
54
|
+
inboxId: inboxId,
|
|
55
|
+
accountAddress,
|
|
56
|
+
isPublic,
|
|
57
|
+
authPolicy,
|
|
58
|
+
encryptionPublicKey,
|
|
59
|
+
signature: {
|
|
60
|
+
hex: signature.toCompactHex(),
|
|
61
|
+
recovery: signature.recovery,
|
|
62
|
+
},
|
|
63
|
+
} satisfies Messages.RequestCreateAccountInbox;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function createSpaceInboxCreationMessage({
|
|
67
|
+
author,
|
|
68
|
+
spaceId,
|
|
69
|
+
isPublic,
|
|
70
|
+
authPolicy,
|
|
71
|
+
spaceSecretKey,
|
|
72
|
+
previousEventHash,
|
|
73
|
+
}: CreateSpaceInboxParams): Promise<Messages.RequestCreateSpaceInboxEvent> {
|
|
74
|
+
// Same as createAccountInboxMessage but with spaceId instead of accountAddress, and generating a keypair for the inbox
|
|
75
|
+
const inboxId = bytesToHex(randomBytes(32));
|
|
76
|
+
const { publicKey, privateKey } = cryptoBoxKeyPair();
|
|
77
|
+
|
|
78
|
+
// encrypt the inbox secret key with the space secret key
|
|
79
|
+
const encryptedInboxSecretKey = Messages.encryptMessage({
|
|
80
|
+
message: privateKey,
|
|
81
|
+
secretKey: hexToBytes(spaceSecretKey),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const spaceEvent = await Effect.runPromise(
|
|
85
|
+
SpaceEvents.createInbox({
|
|
86
|
+
spaceId,
|
|
87
|
+
inboxId,
|
|
88
|
+
encryptionPublicKey: bytesToHex(publicKey),
|
|
89
|
+
secretKey: bytesToHex(encryptedInboxSecretKey),
|
|
90
|
+
isPublic,
|
|
91
|
+
authPolicy,
|
|
92
|
+
author,
|
|
93
|
+
previousEventHash,
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
type: 'create-space-inbox-event',
|
|
99
|
+
spaceId,
|
|
100
|
+
event: spaceEvent,
|
|
101
|
+
} satisfies Messages.RequestCreateSpaceInboxEvent;
|
|
102
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import * as Messages from '../messages/index.js';
|
|
3
|
+
|
|
4
|
+
export const listPublicSpaceInboxes = async ({
|
|
5
|
+
spaceId,
|
|
6
|
+
syncServerUri,
|
|
7
|
+
}: Readonly<{ spaceId: string; syncServerUri: string }>): Promise<readonly Messages.SpaceInboxPublic[]> => {
|
|
8
|
+
const res = await fetch(new URL(`/spaces/${spaceId}/inboxes`, syncServerUri), {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
});
|
|
11
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseListSpaceInboxesPublic)(await res.json());
|
|
12
|
+
return decoded.inboxes;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const listPublicAccountInboxes = async ({
|
|
16
|
+
accountAddress,
|
|
17
|
+
syncServerUri,
|
|
18
|
+
}: Readonly<{ accountAddress: string; syncServerUri: string }>): Promise<readonly Messages.AccountInboxPublic[]> => {
|
|
19
|
+
const res = await fetch(new URL(`/accounts/${accountAddress}/inboxes`, syncServerUri), {
|
|
20
|
+
method: 'GET',
|
|
21
|
+
});
|
|
22
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseListAccountInboxesPublic)(await res.json());
|
|
23
|
+
return decoded.inboxes;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getSpaceInbox = async ({
|
|
27
|
+
spaceId,
|
|
28
|
+
inboxId,
|
|
29
|
+
syncServerUri,
|
|
30
|
+
}: Readonly<{ spaceId: string; inboxId: string; syncServerUri: string }>): Promise<Messages.SpaceInboxPublic> => {
|
|
31
|
+
const res = await fetch(new URL(`/spaces/${spaceId}/inboxes/${inboxId}`, syncServerUri), {
|
|
32
|
+
method: 'GET',
|
|
33
|
+
});
|
|
34
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseSpaceInboxPublic)(await res.json());
|
|
35
|
+
return decoded.inbox;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const getAccountInbox = async ({
|
|
39
|
+
accountAddress,
|
|
40
|
+
inboxId,
|
|
41
|
+
syncServerUri,
|
|
42
|
+
}: Readonly<{
|
|
43
|
+
accountAddress: string;
|
|
44
|
+
inboxId: string;
|
|
45
|
+
syncServerUri: string;
|
|
46
|
+
}>): Promise<Messages.AccountInboxPublic> => {
|
|
47
|
+
const res = await fetch(new URL(`/accounts/${accountAddress}/inboxes/${inboxId}`, syncServerUri), {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
});
|
|
50
|
+
const decoded = Schema.decodeUnknownSync(Messages.ResponseAccountInboxPublic)(await res.json());
|
|
51
|
+
return decoded.inbox;
|
|
52
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './create-inbox.js';
|
|
2
|
+
export * from './get-list-inboxes.js';
|
|
3
|
+
export * from './message-encryption.js';
|
|
4
|
+
export * from './message-validation.js';
|
|
5
|
+
export * from './prepare-message.js';
|
|
6
|
+
export * from './recover-inbox-creator.js';
|
|
7
|
+
export * from './recover-inbox-message-signer.js';
|
|
8
|
+
export * from './send-message.js';
|
|
9
|
+
export * from './merge-messages.js';
|
|
10
|
+
export * from './types.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { InboxMessageStorageEntry } from '../store.js';
|
|
2
|
+
|
|
3
|
+
export function mergeMessages(
|
|
4
|
+
existingMessages: InboxMessageStorageEntry[],
|
|
5
|
+
existingSeenIds: Set<string>,
|
|
6
|
+
newMessages: InboxMessageStorageEntry[],
|
|
7
|
+
) {
|
|
8
|
+
const messages = [...existingMessages];
|
|
9
|
+
const seenMessageIds = new Set(existingSeenIds);
|
|
10
|
+
|
|
11
|
+
// Filter and add new messages
|
|
12
|
+
const newFilteredMessages = newMessages.filter((msg) => !seenMessageIds.has(msg.id));
|
|
13
|
+
for (const msg of newFilteredMessages) {
|
|
14
|
+
seenMessageIds.add(msg.id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (newFilteredMessages.length > 0) {
|
|
18
|
+
// Only sort if the last new message is earlier than the last existing message
|
|
19
|
+
if (messages.length > 0 && newFilteredMessages[0].createdAt < messages[messages.length - 1].createdAt) {
|
|
20
|
+
messages.push(...newFilteredMessages);
|
|
21
|
+
messages.sort((a, b) => (a.createdAt < b.createdAt ? -1 : 1));
|
|
22
|
+
} else {
|
|
23
|
+
messages.push(...newFilteredMessages);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { messages, seenMessageIds };
|
|
28
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { cryptoBoxSeal, cryptoBoxSealOpen } from '@serenity-kit/noble-sodium';
|
|
2
|
+
import { bytesToHex, hexToBytes, stringToUint8Array, uint8ArrayToString } from '../utils/index.js';
|
|
3
|
+
|
|
4
|
+
type EncryptParams = {
|
|
5
|
+
message: string;
|
|
6
|
+
encryptionPublicKey: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type DecryptParams = {
|
|
10
|
+
ciphertext: string;
|
|
11
|
+
encryptionPrivateKey: string;
|
|
12
|
+
encryptionPublicKey: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function encryptInboxMessage({ message, encryptionPublicKey }: EncryptParams): {
|
|
16
|
+
ciphertext: string;
|
|
17
|
+
} {
|
|
18
|
+
const ciphertext = cryptoBoxSeal({
|
|
19
|
+
message: stringToUint8Array(message),
|
|
20
|
+
publicKey: hexToBytes(encryptionPublicKey),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return { ciphertext: bytesToHex(ciphertext) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function decryptInboxMessage({ ciphertext, encryptionPrivateKey, encryptionPublicKey }: DecryptParams): string {
|
|
27
|
+
const publicKey = hexToBytes(encryptionPublicKey);
|
|
28
|
+
const privateKey = hexToBytes(encryptionPrivateKey);
|
|
29
|
+
const message = cryptoBoxSealOpen({
|
|
30
|
+
ciphertext: hexToBytes(ciphertext),
|
|
31
|
+
privateKey,
|
|
32
|
+
publicKey,
|
|
33
|
+
});
|
|
34
|
+
return uint8ArrayToString(message);
|
|
35
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as Identity from '../identity/index.js';
|
|
2
|
+
import type * as Messages from '../messages/index.js';
|
|
3
|
+
import type { AccountInboxStorageEntry, SpaceInboxStorageEntry } from '../store.js';
|
|
4
|
+
import { recoverAccountInboxMessageSigner, recoverSpaceInboxMessageSigner } from './recover-inbox-message-signer.js';
|
|
5
|
+
|
|
6
|
+
export const validateSpaceInboxMessage = async (
|
|
7
|
+
message: Messages.InboxMessage,
|
|
8
|
+
inbox: SpaceInboxStorageEntry,
|
|
9
|
+
spaceId: string,
|
|
10
|
+
syncServerUri: string,
|
|
11
|
+
) => {
|
|
12
|
+
if (message.signature) {
|
|
13
|
+
if (inbox.authPolicy === 'anonymous') {
|
|
14
|
+
console.error('Signed message in anonymous inbox');
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (!message.authorAccountAddress) {
|
|
18
|
+
console.error('Signed message without authorAccountAddress');
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const signer = recoverSpaceInboxMessageSigner(message, spaceId, inbox.inboxId);
|
|
22
|
+
const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountAddress, syncServerUri);
|
|
23
|
+
const isValid = signer === verifiedIdentity.signaturePublicKey;
|
|
24
|
+
if (!isValid) {
|
|
25
|
+
console.error('Invalid signature', signer, verifiedIdentity.signaturePublicKey);
|
|
26
|
+
}
|
|
27
|
+
return isValid;
|
|
28
|
+
}
|
|
29
|
+
// Unsigned message is valid if the inbox is anonymous or optional auth
|
|
30
|
+
const isValid = inbox.authPolicy !== 'requires_auth';
|
|
31
|
+
if (!isValid) {
|
|
32
|
+
console.error('Unsigned message in required auth inbox');
|
|
33
|
+
}
|
|
34
|
+
return isValid;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const validateAccountInboxMessage = async (
|
|
38
|
+
message: Messages.InboxMessage,
|
|
39
|
+
inbox: AccountInboxStorageEntry,
|
|
40
|
+
accountAddress: string,
|
|
41
|
+
syncServerUri: string,
|
|
42
|
+
) => {
|
|
43
|
+
if (message.signature) {
|
|
44
|
+
if (inbox.authPolicy === 'anonymous') {
|
|
45
|
+
console.error('Signed message in anonymous inbox');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (!message.authorAccountAddress) {
|
|
49
|
+
console.error('Signed message without authorAccountAddress');
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const signer = recoverAccountInboxMessageSigner(message, accountAddress, inbox.inboxId);
|
|
53
|
+
const verifiedIdentity = await Identity.getVerifiedIdentity(message.authorAccountAddress, syncServerUri);
|
|
54
|
+
const isValid = signer === verifiedIdentity.signaturePublicKey;
|
|
55
|
+
if (!isValid) {
|
|
56
|
+
console.error('Invalid signature', signer, verifiedIdentity.signaturePublicKey);
|
|
57
|
+
}
|
|
58
|
+
return isValid;
|
|
59
|
+
}
|
|
60
|
+
// Unsigned message is valid if the inbox is anonymous or optional auth
|
|
61
|
+
const isValid = inbox.authPolicy !== 'requires_auth';
|
|
62
|
+
if (!isValid) {
|
|
63
|
+
console.error('Unsigned message in required auth inbox');
|
|
64
|
+
}
|
|
65
|
+
return isValid;
|
|
66
|
+
};
|