{"id":26552086,"url":"https://github.com/iartem/artree","last_synced_at":"2025-03-22T08:23:41.001Z","repository":{"id":246822172,"uuid":"820851510","full_name":"iartem/artree","owner":"iartem","description":"Asynchronous Ratcheting Tree implementation in Typescript","archived":false,"fork":false,"pushed_at":"2024-07-04T07:47:19.000Z","size":373,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-18T03:46:56.338Z","etag":null,"topics":["art","communication","double-ratchet","encryption","messaging","public-key-exchange","ratchet","x3dh"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/iartem.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-27T09:55:52.000Z","updated_at":"2024-07-04T07:46:14.000Z","dependencies_parsed_at":null,"dependency_job_id":"b4f54ce7-634f-4855-bbce-3b1c42e7d84f","html_url":"https://github.com/iartem/artree","commit_stats":null,"previous_names":["iartem/artree"],"tags_count":5,"template":false,"template_full_name":"ryansonshine/typescript-npm-package-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iartem%2Fartree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iartem%2Fartree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iartem%2Fartree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/iartem%2Fartree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/iartem","download_url":"https://codeload.github.com/iartem/artree/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244908095,"owners_count":20529977,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["art","communication","double-ratchet","encryption","messaging","public-key-exchange","ratchet","x3dh"],"created_at":"2025-03-22T08:23:40.493Z","updated_at":"2025-03-22T08:23:40.987Z","avatar_url":"https://github.com/iartem.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ARTree\n\n[![npm package][npm-img]][npm-url]\n[![Build Status][build-img]][build-url]\n[![Downloads][downloads-img]][downloads-url]\n[![Issues][issues-img]][issues-url]\n[![Code Coverage][codecov-img]][codecov-url]\n[![Commitizen Friendly][commitizen-img]][commitizen-url]\n[![Semantic Release][semantic-release-img]][semantic-release-url]\n\n\u003e Asynchronous Ratcheting Tree implementation in Typescript\n\nAsyncronous Ratcheting Tree (see [ART paper](https://eprint.iacr.org/2017/666)) is PKE and key derivation protocol which allows group of users to agree on using one common key while keeping forward secrecy and asynchronous nature of group communcation.\n\n**TLDR:** a binary tree of Diffie Hellman keys. Each node is HKDF(DH(A, B)) where A \u0026 B are keys of 2 of its children.\n\n\u003cfigure\u003e\n  \u003cimg\n  src=\"/assets/tree.png\"\n  alt=\"DH binary tree\"\u003e\n  \u003cfigcaption\u003eFigure courtesy of https://eprint.iacr.org/2017/666\u003c/figcaption\u003e\n\u003c/figure\u003e\n\n\u003cbr/\u003e\n\nThe reasons why ART is preferred over over plain ratchets, double ratchets and alike, include:\n\n1. In order to calculate stage (top level) key you need a secret key of one of the leaves and public keys of other nodes. Single secret key is required and is enough.\n2. Parent node keys can be calculated by either of its children with all of them ending up with the same top level key.\n3. Any leaf can change its key asynchronously. While some members are offline, for example.\n4. One key! No need to send N - 1 messages for any message in group of N members.\n\nThis implementation is largely based on [ART paper](https://eprint.iacr.org/2017/666), yet it adds several extra features (use with caution, not verified or scientifically proven!) necessary for real world application:\n\n- Ability to split tree leaves and add new users.\n- Ability to redistribute up to date tree to users who were offline while other memebers updated their keys. Useful when you don't store messages.\n\n\u003e [!WARNING]\n\u003e Note that ARTree does not encrypt messages it produces. It's implied that **\u003cu\u003epackage user will encrypt messages\u003c/u\u003e** between ART members with `art.stage` key. Both synchronous \u0026 asynchronous encryption is supported as `art.stage` is a keypair. That being said, ARTree actually uses encryption in one place: it encrypts stage key transferred to new tree member on split; it does that because encryption key would be tricky to calculate outside of the library.\n\n## Getting started\n\n### Install\n\n```bash\nnpm install artree\n```\n\n### Add your crypto implementaion\n\nARTree is BYOCrypto, abstracted from crypto implementation, but tested with awesome [paulmillr](https://github.com/paulmillr)'s [@noble](https://github.com/paulmillr/noble-curves) set of packages:\n\n```bash\nnpm install @noble/curves @noble/hashes @noble/ciphers\n```\n\n```ts\nimport { gcm } from '@noble/ciphers/aes';\nimport { secp256k1 } from '@noble/curves/secp256k1';\nimport { hkdf } from '@noble/hashes/hkdf';\nimport { sha256 } from '@noble/hashes/sha256';\nimport { randomBytes } from 'crypto';\nimport { setCrypto, SK, SI, PK, concat } from 'artree';\n\nsetCrypto({\n  generateSecretKey: secp256k1.utils.randomPrivateKey,\n  derivePublicKey: secp256k1.getPublicKey,\n  getSharedSecret: secp256k1.getSharedSecret,\n  hash_256: sha256,\n  sign: function (data: Uint8Array, sk: SK) {\n    return secp256k1.sign(data, sk, { prehash: true }).toCompactRawBytes();\n  },\n  verify: function (si: SI, data: Uint8Array, pk: PK) {\n    return secp256k1.verify(si, data, pk, { prehash: true });\n  },\n  hkdf: hkdf.bind(null, sha256),\n  encrypt: function (data: Uint8Array, key: SK) {\n    const nonce = randomBytes(this.ENCRYPTION_PREFIX_LENGTH);\n    return concat(nonce, gcm(key, nonce).encrypt(data));\n  },\n  decrypt: function (data: Uint8Array, key: SK) {\n    const nonce = data.subarray(0, this.ENCRYPTION_PREFIX_LENGTH);\n    return gcm(key, nonce).decrypt(data.subarray(this.ENCRYPTION_PREFIX_LENGTH));\n  },\n  PK_LENGTH: 33,\n  SK_LENGTH: 32,\n  SI_LENGTH: 64,\n  ENCRYPTION_PREFIX_LENGTH: 12,\n  ENCRYPTION_SUFFIX_LENGTH: 16,\n});\n```\n\n### Start turning!\n\nIn order to create ART you'd need identity \u0026 ephemeral private keys for initiator and identity \u0026 ephemeral public keys for each participant. Also ephemeral keys must be signed with corresponding identity key to prove it belongs to identity secret key owner:\n\n```ts\nimport { ART, Me, Peer, keypair } from 'artree';\n\nconst keys = new Array(2).fill(0).map(() =\u003e {\n  const identity = keypair(); // {sk: Uint8Array, pk: Uint8Array}, Secret Key \u0026 Public Key\n  const ephemeral = keypair();\n  const ephemeral_signature = secp256k1.sign(sha256(ephemeral.pk), identity.sk).toCompactRawBytes();\n  return { identity, ephemeral, ephemeral_signature };\n});\n\n// Alice initiates ART\nconst alice = ART.initiate(\n  keys.map(({ identity, ephemeral, ephemeral_signature }, i) =\u003e {\n    if (i === 0) {\n      // initiator knows own secret keys\n      return new Me(identity, ephemeral);\n    } else {\n      // initiator knows only public keys \u0026 signature of other members\n      return new Peer(identity.pk, ephemeral.pk, ephemeral_signature);\n    }\n  })\n);\n\n// Alice also generated setup message which she needs to send to other members\n// The message should be encrypted when transferring to them\nconst setupMessage = alice.setupMessage;\n\n// Bob receives setup message and joins the tree\n// He only needs setup message and his identity \u0026 ephemeral keys to start turning the tree\nconst bob = ART.fromSetupMessage(new Me(keys[1]!.identity, keys[1]!.ephemeral), setupMessage);\n\nexpect(bob.stage.sk).toEqual(alice.stage.sk); // look Ma, same keys!\n\n// Once set up, members can turn the ratchet at their will\nconst bobUpdateMessage = bob.updateKey();\nalice.processKeyUpdate(bobUpdateMessage);\n\nexpect(alice.stage.sk).toEqual(bob.stage.sk); // look Ma, same keys!\n```\n\nSee full example [in tests](https://github.com/iartem/artree/blob/main/test/art.spec.ts#L57).\n\n### Tree modifications after initialization\n\nARTree allows new tree members to join the tree after its initialization:\n\n- Any tree member can replace its tree leaf with a node consisting of 2 leaves: old leaf and new leaf with the same identity key as the old one. Think of one user having multiple devices.\n- Tree initiator can add new leaves at arbitrary position.\n\n\u003e [!CAUTION]\n\u003e Using this feature (`art.split()`) is not required and in fact not advised if security is your main concern.\n\nThe problem comes from the fact that the very top level (stage) key is an HKDF which uses previous key as one of its inputs. Therefore adding a tree member requires sharing current stage key with this member in order for newcomer to be able to calculate next stage key.\n\nAlternative to current implementation would be not using HKDF for stage key calculation, but that would come at a cost of forward secrecy.\n\nBottomline: if you don't need to add new tree members after its initialization, better don't.\n\n## API\n\n### Intro\n\n`Peer` - a class with public identity \u0026 ephemeral keys + signature of ephemeral key by identity key.\n\n`Me` - a class with identity \u0026 ephemeral keypair, generates signature automatically.\n\n`SK`, `sk` - secret (private) key.\n\n`PK`, `pk` - public key.\n\n`ART` - main package class.\n\n\u003cbr/\u003e\n\n### `ART.initiate(peers: Peer[]): ART`\n\nCreate new tree for given peers. Called by tree initiator. One of `Peer` objects must be initiator's `Me` instance. Returns ready to use tree with `setupMessage` property set to a message to be sent to other members.\n\n\u003cbr/\u003e\n\n### `ART.fromSetupMessage(me: Me, message: Uint8Array): ART`\n\nRecreate tree at non-initiator side from setup message.\n\n\u003cbr/\u003e\n\n### `ART.fromSplitMessage(me: Me, snapshot: Uint8Array, splitMessage: Uint8Array): ART`\n\n**USE WITH CAUTION** Recreate tree at non-initiator side from split snapshot \u0026 message. Used to add new members to the tree after its initiation. `snapshot` \u0026 `splitMessage` are results of `art.split()` call.\n\n\u003cbr/\u003e\n\n### `art.updateKey(key = keypair()): Uint8Array`\n\nReplace ephemeral key of a tree member with new one. Returns a message which should be sent to every other tree member. Failure to do so will make them unable to process next messages as their stage key will be outdated. By default `key` is randomly generated.\n\n\u003cbr/\u003e\n\n### `art.processKeyUpdate(message: Uint8Array): void`\n\nProcess key update message from another peer.\n\n\u003cbr/\u003e\n\n### `art.split(peer: Peer, at?: Uint8Array): {message: Uint8Array, snapshot: Uint8Array}`\n\n**USE WITH CAUTION** Splits one of the leaves into two, adding new member to the tree. Initiator can add members at arbitrary positions (by setting `at` parameter to leaf's `pk`), while other members can only add new members with the same identity key right next to their own node (`at === undefined`). See [Tree modifications after initialization](#tree-modifications-after-initialization) for details.\n\n\u003cbr/\u003e\n\n### `art.processSplit(message: Uint8Array)`\n\n**USE WITH CAUTION** Updates tree with new Peer. See [Tree modifications after initialization](#tree-modifications-after-initialization) for details.\n\n[build-img]: https://github.com/iartem/artree/actions/workflows/release.yml/badge.svg\n[build-url]: https://github.com/iartem/artree/actions/workflows/release.yml\n[downloads-img]: https://img.shields.io/npm/dt/artree\n[downloads-url]: https://www.npmtrends.com/artree\n[npm-img]: https://img.shields.io/npm/v/artree\n[npm-url]: https://www.npmjs.com/package/artree\n[issues-img]: https://img.shields.io/github/issues/iartem/artree\n[issues-url]: https://github.com/iartem/artree/issues\n[codecov-img]: https://codecov.io/gh/iartem/artree/branch/main/graph/badge.svg\n[codecov-url]: https://codecov.io/gh/iartem/artree\n[semantic-release-img]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg\n[semantic-release-url]: https://github.com/semantic-release/semantic-release\n[commitizen-img]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg\n[commitizen-url]: http://commitizen.github.io/cz-cli/\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiartem%2Fartree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fiartem%2Fartree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fiartem%2Fartree/lists"}