├── .depcheckrc ├── .github └── workflows │ └── sync.yml ├── LICENSE ├── Readme.md ├── packages ├── addressing │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── addressCasing.test.ts │ │ ├── addressCasing.ts │ │ ├── addressComparison.test.ts │ │ ├── addressComparison.ts │ │ ├── addressFormatting.test.ts │ │ ├── addressFormatting.ts │ │ ├── addressFormattingRule.ts │ │ ├── addressFromPublicKey.test.ts │ │ ├── addressFromPublicKey.ts │ │ ├── addressPredicates.test.ts │ │ ├── addressPredicates.ts │ │ ├── checkAddressForErrors.test.ts │ │ ├── checkAddressForErrors.ts │ │ ├── encoding.test.ts │ │ ├── encoding.ts │ │ ├── errors.ts │ │ ├── formatMailLike.ts │ │ ├── index.ts │ │ ├── nameServiceAddress.ts │ │ ├── nameservices │ │ │ ├── index.ts │ │ │ ├── matchesNameservice.test.ts │ │ │ ├── matchesNameservice.ts │ │ │ └── nameserviceDescriptions.ts │ │ ├── parseWalletAddress.ts │ │ ├── protocols │ │ │ ├── consts.ts │ │ │ ├── errors.ts │ │ │ ├── ethereum │ │ │ │ ├── address.test.ts │ │ │ │ ├── address.ts │ │ │ │ └── test.const.ts │ │ │ ├── filecoin │ │ │ │ ├── address.ts │ │ │ │ ├── const.ts │ │ │ │ ├── delegatedAddress.test.ts │ │ │ │ ├── delegatedAddress.ts │ │ │ │ └── types.ts │ │ │ ├── index.ts │ │ │ ├── near │ │ │ │ └── address.ts │ │ │ ├── solana │ │ │ │ ├── address.test.ts │ │ │ │ ├── address.ts │ │ │ │ └── test.const.ts │ │ │ └── tezos │ │ │ │ ├── address.test.ts │ │ │ │ ├── address.ts │ │ │ │ ├── const.ts │ │ │ │ └── test.const.ts │ │ ├── test.constants.ts │ │ ├── walletAddress.test.ts │ │ └── walletAddress.ts │ └── tsconfig.json ├── api │ ├── .gitignore │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── axios │ │ │ ├── axios.test.ts │ │ │ ├── axios.ts │ │ │ ├── config.ts │ │ │ ├── index.ts │ │ │ ├── token.test.ts │ │ │ └── token.ts │ │ ├── helpers │ │ │ ├── address.ts │ │ │ ├── apiKeyToCryptoKey.ts │ │ │ ├── converters.test.ts │ │ │ ├── cryptoKeyToApiKey.ts │ │ │ ├── encoding.ts │ │ │ ├── errors.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── jwt │ │ │ ├── index.ts │ │ │ ├── jwt.test.ts │ │ │ └── jwt.ts │ └── tsconfig.json ├── crypto │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── cipher │ │ │ ├── cipher.ts │ │ │ ├── ecdh │ │ │ │ ├── ecdh.test.ts │ │ │ │ ├── ecdh.ts │ │ │ │ ├── ed25519.test.ts │ │ │ │ ├── ed25519.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── keyExchange.ts │ │ │ └── nacl │ │ │ │ ├── index.ts │ │ │ │ ├── privateKeyDecrypter.test.ts │ │ │ │ ├── privateKeyDecrypter.ts │ │ │ │ ├── privateKeyEncrypter.test.ts │ │ │ │ ├── privateKeyEncrypter.ts │ │ │ │ ├── publicKeyDecrypter.test.ts │ │ │ │ ├── publicKeyDecrypter.ts │ │ │ │ ├── publicKeyEncrypter.test.ts │ │ │ │ ├── publicKeyEncrypter.ts │ │ │ │ ├── publicKeyEndToEnd.test.ts │ │ │ │ ├── secretbox.test.ts │ │ │ │ ├── secretbox.ts │ │ │ │ ├── serialization.test.ts │ │ │ │ └── serialization.ts │ │ ├── ed25519 │ │ │ ├── derivation.test.ts │ │ │ ├── derivation.ts │ │ │ ├── hd.test.ts │ │ │ ├── hd.ts │ │ │ ├── index.ts │ │ │ ├── private.test.ts │ │ │ ├── private.ts │ │ │ ├── public.test.ts │ │ │ ├── public.ts │ │ │ └── test.const.ts │ │ ├── errors.ts │ │ ├── hd.test.ts │ │ ├── hd.ts │ │ ├── index.ts │ │ ├── keys.ts │ │ ├── mnemonic │ │ │ ├── mnemonic.test.ts │ │ │ └── mnemonic.ts │ │ ├── multikey │ │ │ ├── bytes.test.ts │ │ │ ├── bytes.ts │ │ │ ├── ids.ts │ │ │ ├── index.ts │ │ │ └── names.ts │ │ ├── private.ts │ │ ├── public.ts │ │ ├── rand.test.const.ts │ │ ├── rand.ts │ │ ├── scrypt │ │ │ ├── index.ts │ │ │ ├── scrypt.test.ts │ │ │ └── scrypt.ts │ │ ├── secp256k1 │ │ │ ├── index.ts │ │ │ ├── private.test.ts │ │ │ ├── private.ts │ │ │ ├── public.test.ts │ │ │ ├── public.ts │ │ │ └── test.const.ts │ │ ├── secp256r1 │ │ │ ├── index.ts │ │ │ ├── private.test.ts │ │ │ ├── private.ts │ │ │ ├── public.test.ts │ │ │ ├── public.ts │ │ │ └── test.const.ts │ │ └── testing │ │ │ ├── index.ts │ │ │ ├── private.ts │ │ │ └── public.ts │ └── tsconfig.json ├── encoding │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── ascii.test.ts │ │ ├── ascii.ts │ │ ├── base32.test.ts │ │ ├── base32.ts │ │ ├── base58.test.ts │ │ ├── base58.ts │ │ ├── base58Check.ts │ │ ├── base64.test.ts │ │ ├── base64.ts │ │ ├── consts.ts │ │ ├── encoding.test.ts │ │ ├── encoding.ts │ │ ├── ensure.ts │ │ ├── hex.test.ts │ │ ├── hex.ts │ │ ├── index.ts │ │ ├── utf8.test.ts │ │ └── utf8.ts │ └── tsconfig.json ├── internal │ ├── .gitignore │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── rollup.const.js │ ├── src │ │ ├── configuration.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ ├── unexpected.ts │ │ │ └── validation.ts │ │ ├── formatters │ │ │ ├── __snapshots__ │ │ │ │ ├── generate.test.ts.snap │ │ │ │ └── parse.test.ts.snap │ │ │ ├── __tests__ │ │ │ │ └── legacy-content-and-subject.eml │ │ │ ├── consts.ts │ │ │ ├── generate.test.ts │ │ │ ├── generate.ts │ │ │ ├── parse.test.ts │ │ │ ├── parse.ts │ │ │ ├── roundtrip.test.ts │ │ │ ├── simpleMimeHeaderParser.test.ts │ │ │ └── simpleMimeHeaderParser.ts │ │ ├── identityKeys │ │ │ ├── identityKeys.test.ts │ │ │ ├── identityKeys.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── mailbox │ │ │ ├── __snapshots__ │ │ │ │ ├── addressHasher.test.ts.snap │ │ │ │ └── mailboxOperations.test.ts.snap │ │ │ ├── addressHasher.test.ts │ │ │ ├── addressHasher.ts │ │ │ ├── addressIdentityKeyResolver.test.ts │ │ │ ├── addressIdentityKeyResolver.ts │ │ │ ├── index.ts │ │ │ ├── mailboxOperations.test.ts │ │ │ ├── mailboxOperations.ts │ │ │ ├── mailboxStorage.test.ts │ │ │ ├── mailboxStorage.ts │ │ │ ├── messageCrypto.test.ts │ │ │ ├── messageCrypto.ts │ │ │ ├── messageId.test.ts │ │ │ ├── messageId.ts │ │ │ ├── messageMailboxOwnerMatcher.test.ts │ │ │ ├── messageMailboxOwnerMatcher.ts │ │ │ ├── migrations.test.ts │ │ │ ├── migrations.ts │ │ │ ├── payloadStorage │ │ │ │ ├── index.ts │ │ │ │ ├── mailchainPayloadStorage.test.ts │ │ │ │ ├── mailchainPayloadStorage.ts │ │ │ │ └── payloadStorage.ts │ │ │ ├── types.ts │ │ │ ├── userMailboxHasher.test.ts │ │ │ ├── userMailboxHasher.ts │ │ │ └── versioning.md │ │ ├── mailboxRuleEngine │ │ │ ├── actions.ts │ │ │ ├── actionsHandler.test.ts │ │ │ ├── actionsHandler.ts │ │ │ ├── conditions.ts │ │ │ ├── conditionsHandler.test.ts │ │ │ ├── conditionsHandler.ts │ │ │ ├── index.ts │ │ │ ├── mailboxRuleEngine.test.ts │ │ │ ├── mailboxRuleEngine.ts │ │ │ ├── mailchainRuleRepository.test.ts │ │ │ ├── mailchainRuleRepository.ts │ │ │ ├── mailchainUserBlocklist.test.ts │ │ │ ├── mailchainUserBlocklist.ts │ │ │ └── rule.ts │ │ ├── mailchainResult.ts │ │ ├── messageSync │ │ │ ├── index.ts │ │ │ ├── messageSync.test.ts │ │ │ ├── messageSync.ts │ │ │ ├── previousMessageSync.test.ts │ │ │ └── previousMessageSync.ts │ │ ├── messagingKeys │ │ │ ├── addressNonce.test.ts │ │ │ ├── addressNonce.ts │ │ │ ├── contractResolvers │ │ │ │ ├── errors.ts │ │ │ │ ├── mailchain.ts │ │ │ │ ├── near.ts │ │ │ │ └── resolver.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── messagingKeyContract.test.ts │ │ │ ├── messagingKeyContract.ts │ │ │ ├── messagingKeys.test.ts │ │ │ ├── messagingKeys.ts │ │ │ ├── privateMessagingKeys.test.ts │ │ │ ├── privateMessagingKeys.ts │ │ │ ├── proof.ts │ │ │ └── verify.ts │ │ ├── metadata │ │ │ ├── index.ts │ │ │ └── metadata.ts │ │ ├── migration.test.ts │ │ ├── migration.ts │ │ ├── nameservices │ │ │ ├── index.ts │ │ │ ├── nameservices.test.ts │ │ │ └── nameservices.ts │ │ ├── receiving │ │ │ ├── deliveryRequests │ │ │ │ ├── deliveryRequests.test.ts │ │ │ │ ├── deliveryRequests.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mail │ │ │ │ ├── index.ts │ │ │ │ ├── mail.test.ts │ │ │ │ └── mail.ts │ │ │ ├── mailer │ │ │ │ ├── author.ts │ │ │ │ ├── index.ts │ │ │ │ └── mailer.ts │ │ │ └── payload │ │ │ │ ├── index.ts │ │ │ │ ├── payload.test.ts │ │ │ │ └── payload.ts │ │ ├── sending │ │ │ ├── deliveryRequests │ │ │ │ ├── deliveryRequests.test.ts │ │ │ │ ├── deliveryRequests.ts │ │ │ │ └── index.ts │ │ │ ├── distributor │ │ │ │ ├── deliveryRequests.ts │ │ │ │ ├── distributor.test.ts │ │ │ │ ├── distributor.ts │ │ │ │ └── index.ts │ │ │ ├── errors.ts │ │ │ ├── index.ts │ │ │ ├── mail │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── payloads.test.ts.snap │ │ │ │ ├── convertSendMailParams.ts │ │ │ │ ├── index.ts │ │ │ │ ├── payloads.test.ts │ │ │ │ ├── payloads.ts │ │ │ │ ├── prepare.test.ts │ │ │ │ ├── prepare.ts │ │ │ │ ├── sendMailParams.ts │ │ │ │ ├── sender.test.ts │ │ │ │ └── sender.ts │ │ │ ├── payload │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── create.test.ts.snap │ │ │ │ ├── create.test.ts │ │ │ │ ├── create.ts │ │ │ │ ├── index.ts │ │ │ │ ├── store.test.ts │ │ │ │ └── store.ts │ │ │ └── verifiablePresentationRequest │ │ │ │ ├── index.ts │ │ │ │ ├── payload.ts │ │ │ │ ├── sender.test.ts │ │ │ │ └── sender.ts │ │ ├── test.const.ts │ │ ├── transport │ │ │ ├── deliveryRequests │ │ │ │ ├── delivery.ts │ │ │ │ ├── envelope.test.ts │ │ │ │ ├── envelope.ts │ │ │ │ ├── index.ts │ │ │ │ ├── keybundle.test.ts │ │ │ │ └── keybundle.ts │ │ │ ├── distribution │ │ │ │ ├── distribution.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── mail │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── mailer │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── content.test.ts.snap │ │ │ │ ├── author.ts │ │ │ │ ├── content.test.ts │ │ │ │ ├── content.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mailerProof.test.ts │ │ │ │ ├── mailerProof.ts │ │ │ │ ├── payload.ts │ │ │ │ └── types.ts │ │ │ ├── payload │ │ │ │ ├── headers.ts │ │ │ │ ├── headersSerialize.test.ts │ │ │ │ ├── headersSerialize.ts │ │ │ │ ├── index.ts │ │ │ │ ├── payload.ts │ │ │ │ ├── verifier.test.ts │ │ │ │ └── verifier.ts │ │ │ ├── serialization │ │ │ │ ├── chunk.test.ts │ │ │ │ ├── chunk.ts │ │ │ │ ├── decrypt.test.ts │ │ │ │ ├── decrypt.ts │ │ │ │ ├── encrypt.test.ts │ │ │ │ ├── encrypt.ts │ │ │ │ ├── headers.ts │ │ │ │ ├── index.ts │ │ │ │ ├── payload.ts │ │ │ │ ├── serialization.test.ts │ │ │ │ └── serialization.ts │ │ │ └── verifier │ │ │ │ ├── index.ts │ │ │ │ └── sender.ts │ │ ├── user │ │ │ ├── consolidateMailbox.test.ts │ │ │ ├── consolidateMailbox.ts │ │ │ ├── createAlias.ts │ │ │ ├── index.ts │ │ │ ├── migrations.test.ts │ │ │ ├── migrations.ts │ │ │ ├── test.const.ts │ │ │ ├── types.ts │ │ │ ├── userProfile.test.ts │ │ │ ├── userProfile.ts │ │ │ ├── utils.ts │ │ │ └── versioning.md │ │ └── verifiableCredentials │ │ │ ├── __snapshots__ │ │ │ ├── payload.test.ts.snap │ │ │ ├── presentation.test.ts.snap │ │ │ └── resolver.test.ts.snap │ │ │ ├── context.ts │ │ │ ├── did.test.ts │ │ │ ├── did.ts │ │ │ ├── index.ts │ │ │ ├── issuer.ts │ │ │ ├── jwt.ts │ │ │ ├── parseVerifiablePresentationRequest.test.ts │ │ │ ├── parseVerifiablePresentationRequest.ts │ │ │ ├── payload.test.ts │ │ │ ├── payload.ts │ │ │ ├── presentation.test.ts │ │ │ ├── presentation.ts │ │ │ ├── request │ │ │ ├── index.ts │ │ │ └── request.ts │ │ │ ├── resolver.test.ts │ │ │ ├── resolver.ts │ │ │ ├── subject.ts │ │ │ ├── termsOfUse.ts │ │ │ └── verifiableMailchainAddressOwner │ │ │ ├── __snapshots__ │ │ │ ├── issuer.test.ts.snap │ │ │ └── verifier.test.ts.snap │ │ │ ├── factory.test.ts │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ ├── issuer.test.ts │ │ │ ├── issuer.ts │ │ │ ├── verifier.test.ts │ │ │ └── verifier.ts │ └── tsconfig.json ├── keyring │ ├── .eslintignore │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── __snapshots__ │ │ │ └── keyring.test.ts.snap │ │ ├── constants.ts │ │ ├── functions.ts │ │ ├── index.ts │ │ ├── keyring.test.ts │ │ ├── keyring.ts │ │ └── test.const.ts │ └── tsconfig.json ├── message-composer │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── __snapshots__ │ │ │ ├── contentHandler.test.ts.snap │ │ │ ├── headerHandler.test.ts.snap │ │ │ └── messageComposer.test.ts.snap │ │ ├── __tests__ │ │ │ ├── .gitignore │ │ │ └── mailchain-logo.png │ │ ├── consts.ts │ │ ├── contentHandler.test.ts │ │ ├── contentHandler.ts │ │ ├── fallback.ts │ │ ├── folding.ts │ │ ├── hasOnlyAscii.ts │ │ ├── headerFactories.ts │ │ ├── headerHandler.test.ts │ │ ├── headerHandler.ts │ │ ├── headerOrder.ts │ │ ├── index.ts │ │ ├── messageComposer.test.ts │ │ ├── messageComposer.ts │ │ ├── messageComposerContext.ts │ │ └── types.ts │ └── tsconfig.json ├── sdk │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── jest.setup.js │ ├── package.json │ ├── rollup.const.js │ ├── src │ │ ├── index.ts │ │ ├── internal │ │ │ ├── addressNonce.ts │ │ │ ├── index.ts │ │ │ ├── keys.test.ts │ │ │ ├── keys.ts │ │ │ ├── privateMessagingKey.ts │ │ │ └── resolvers.ts │ │ ├── mailchain.ts │ │ └── types.ts │ └── tsconfig.json ├── signatures │ ├── .gitignore │ ├── LICENSE │ ├── Readme.md │ ├── babel.config.cjs │ ├── jest.config.cjs │ ├── package.json │ ├── src │ │ ├── __snapshots__ │ │ │ └── mailer.test.ts.snap │ │ ├── consts.ts │ │ ├── errors.ts │ │ ├── eth_personal.test.ts │ │ ├── eth_personal.ts │ │ ├── index.ts │ │ ├── keyreg │ │ │ ├── __snapshots__ │ │ │ │ └── message.test.ts.snap │ │ │ ├── index.ts │ │ │ ├── message.test.ts │ │ │ ├── message.ts │ │ │ ├── params.ts │ │ │ └── templates.ts │ │ ├── mailchain_message_confirmation.test.ts │ │ ├── mailchain_message_confirmation.ts │ │ ├── mailchain_msgkey.test.ts │ │ ├── mailchain_msgkey.ts │ │ ├── mailchain_password_reset.test.ts │ │ ├── mailchain_password_reset.ts │ │ ├── mailchain_username.test.ts │ │ ├── mailchain_username.ts │ │ ├── mailer.test.ts │ │ ├── mailer.ts │ │ ├── publickey │ │ │ ├── ethereum.test.ts │ │ │ ├── ethereum.ts │ │ │ └── index.ts │ │ ├── raw_ed25519.test.ts │ │ ├── raw_ed25519.ts │ │ ├── tezos_micheline.test.ts │ │ ├── tezos_micheline.ts │ │ └── verify.ts │ └── tsconfig.json └── tsconfig.json └── tsconfig.json /.depcheckrc: -------------------------------------------------------------------------------- 1 | ignores: ['protobufjs', 'long', 'jest-environment-jsdom'] 2 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request_target: 3 | jobs: 4 | sync-repo: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | with: 9 | fetch-depth: 0 10 | 11 | - uses: mailchain/copybara-action@1.0 12 | with: 13 | ssh_key: ${{ secrets.SSH_KEY }} 14 | access_token: ${{ secrets.ACCESS_TOKEN }} 15 | sot_repo: mailchain/monorepo 16 | destination_repo: mailchain/mailchain-sdk-js 17 | pr_include: '**' 18 | push_include: 'packages/sdk-js/**' 19 | pr_move: | 20 | ||packages/sdk-js 21 | -------------------------------------------------------------------------------- /packages/addressing/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Addressing 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/addressing 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/addressing 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The addressing package is used by `@mailchain/sdk` to support securely sending and resolving blockchain addresses for the Mailchain protocol. 24 | -------------------------------------------------------------------------------- /packages/addressing/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/addressing/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/addressing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/addressing", 3 | "version": "0.31.0", 4 | "description": "Mailchain addressing", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "addressing" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@ethersproject/transactions": "^5.7.0", 21 | "@mailchain/crypto": "0.31.0", 22 | "@mailchain/encoding": "0.31.0", 23 | "@noble/hashes": "^1.3.0", 24 | "lodash": "^4.17.21" 25 | }, 26 | "scripts": { 27 | "build": "run -T tsup", 28 | "test": "run -T jest", 29 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 30 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/addressing/src/addressCasing.test.ts: -------------------------------------------------------------------------------- 1 | import { casingByProtocol } from './addressCasing'; 2 | import { ALGORAND, ETHEREUM, MAILCHAIN, NEAR, ProtocolType, SUBSTRATE } from './protocols'; 3 | 4 | const formatMailchainValueTests: { testName: string; value: string; protocol: ProtocolType; expected: string }[] = [ 5 | { 6 | testName: 'should have lower case for mailchain protocol', 7 | value: 'AlIcE', 8 | protocol: MAILCHAIN, 9 | expected: 'alice', 10 | }, 11 | { 12 | testName: 'should have lower case for ethereum protocol', 13 | value: '0x0E5736FBD198496Ef9A890a8D8be7538De9B2e0f', 14 | protocol: ETHEREUM, 15 | expected: '0x0e5736fbd198496ef9a890a8d8be7538de9b2e0f', 16 | }, 17 | { 18 | testName: 'should have mixed case for substrate protocol', 19 | value: '5F3sa2TJAWMqDhXG6jhV4N8ko9SxwGy8TpaNS1repo5EYjQX', 20 | protocol: SUBSTRATE, 21 | expected: '5F3sa2TJAWMqDhXG6jhV4N8ko9SxwGy8TpaNS1repo5EYjQX', 22 | }, 23 | { 24 | testName: 'should have lower case for algorand protocol', 25 | value: 'Z4X3PU5L6X3IPHE3CB5IEIS52TE5GU3GZFOOWW7BOTVXK5BCYR3444OQDW', 26 | protocol: ALGORAND, 27 | expected: 'z4x3pu5l6x3iphe3cb5ieis52te5gu3gzfooww7botvxk5bcyr3444oqdw', 28 | }, 29 | { 30 | testName: 'should have lower case for near protocol', 31 | value: '98793CD91A3F870FB126F66285808C7E094AFCFC4EDA8A970F6648CDF0DBD6DE', 32 | protocol: NEAR, 33 | expected: '98793cd91a3f870fb126f66285808c7e094afcfc4eda8a970f6648cdf0dbd6de', 34 | }, 35 | { testName: '', value: '', protocol: MAILCHAIN, expected: '' }, 36 | ]; 37 | 38 | test.each(formatMailchainValueTests)('$testName', ({ value, protocol, expected }) => { 39 | expect(casingByProtocol(value, protocol)).toEqual(expected); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/addressing/src/addressCasing.ts: -------------------------------------------------------------------------------- 1 | import { ALGORAND, ETHEREUM, MAILCHAIN, NEAR, ProtocolType, SOLANA, SUBSTRATE, TEZOS } from './protocols'; 2 | 3 | export function casingByProtocol(value: string, protocol: ProtocolType): string { 4 | switch (protocol) { 5 | case MAILCHAIN: 6 | return value.toLowerCase(); // case insensitive 7 | case ETHEREUM: 8 | return value.toLowerCase(); // case insensitive 9 | case SOLANA: 10 | case SUBSTRATE: 11 | case TEZOS: 12 | return value; // substrate&tezos encoding is case sensitive 13 | case ALGORAND: 14 | return value.toLowerCase(); // case insensitive 15 | case NEAR: 16 | return value.toLowerCase(); 17 | default: 18 | throw new Error(`casing for protocol [${protocol}] not defined`); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/addressing/src/addressFormatting.ts: -------------------------------------------------------------------------------- 1 | import { humanNameServiceFormatters } from './addressFormattingRule'; 2 | import { formatMailLike } from './formatMailLike'; 3 | import { isNameServiceAddress, NameServiceAddress as MailchainAddress, NameServiceAddress } from './nameServiceAddress'; 4 | 5 | export function formatAddress(address: MailchainAddress, format: 'mail' | 'human-friendly'): string { 6 | if (isNameServiceAddress(address)) return formatNameServiceAddress(address, format); 7 | throw new Error(`unknown format of the provided address [${JSON.stringify(address)}]`); 8 | } 9 | 10 | function formatNameServiceAddress(address: NameServiceAddress, format: 'mail' | 'human-friendly'): string { 11 | switch (format) { 12 | case 'mail': 13 | return formatMailLike(address.username, address.domain); 14 | case 'human-friendly': 15 | for (const formatter of humanNameServiceFormatters) { 16 | const formatted = formatter(address); 17 | if (formatted) return formatted; 18 | } 19 | throw new Error(`failed to format address [${JSON.stringify(address)}]`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/addressing/src/addressFromPublicKey.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceSECP256K1PublicKey, BobSECP256K1PublicKey } from '@mailchain/crypto/secp256k1/test.const'; 2 | import { addressFromPublicKey } from './addressFromPublicKey'; 3 | import { ETHEREUM } from './protocols'; 4 | import { AliceSECP256K1PublicAddress, BobSECP256K1PublicAddress } from './protocols/ethereum/test.const'; 5 | 6 | const testCases = [ 7 | { 8 | caseName: 'ethereum address', 9 | publicKey: AliceSECP256K1PublicKey, 10 | protocol: ETHEREUM, 11 | expectedAddress: AliceSECP256K1PublicAddress, 12 | }, 13 | { 14 | caseName: 'ethereum address', 15 | publicKey: BobSECP256K1PublicKey, 16 | protocol: ETHEREUM, 17 | expectedAddress: BobSECP256K1PublicAddress, 18 | }, 19 | ]; 20 | 21 | test.each(testCases)('$caseName', async ({ publicKey, protocol, expectedAddress }) => { 22 | const result = await addressFromPublicKey(publicKey, protocol); 23 | 24 | expect(result).toEqual(expectedAddress); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/addressing/src/addressFromPublicKey.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@mailchain/crypto'; 2 | import { ETHEREUM, ProtocolType, SOLANA, TEZOS } from './protocols'; 3 | import { addressFromPublicKey as ethereumAddressFromPublicKey } from './protocols/ethereum/address'; 4 | import { tezosAddressFromPublicKey } from './protocols/tezos/address'; 5 | import { solanaAddressFromPublicKey } from './protocols/solana/address'; 6 | 7 | /** 8 | * Derive the address corresponding to the {@link PublicKey}. 9 | * 10 | * @param publicKey the key to derive the address from 11 | * @param protocol the protocol the address should be derived by 12 | * 13 | * @throws Error for unsupported protocols and for protocol unsupported key types. 14 | */ 15 | export async function addressFromPublicKey(publicKey: PublicKey, protocol: ProtocolType): Promise { 16 | switch (protocol) { 17 | case ETHEREUM: 18 | return ethereumAddressFromPublicKey(publicKey); 19 | case TEZOS: 20 | return tezosAddressFromPublicKey(publicKey); 21 | case SOLANA: 22 | return solanaAddressFromPublicKey(publicKey); 23 | default: 24 | throw new Error(`address from PublicKey for {${protocol}} not unsupported`); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/addressing/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class BadlyFormattedAddressError extends Error { 2 | readonly type = 'badly_formatted_address'; 3 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#badly'; 4 | constructor() { 5 | super('Address format is invalid. Check that the format follows the Mailchain address standard.'); 6 | } 7 | } 8 | 9 | export class IdentityProviderAddressInvalidError extends Error { 10 | readonly type = 'identity_provider_address_invalid'; 11 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#identity_provider_address_invalid'; 12 | constructor() { 13 | super(`Address is not valid for the identity provider. Check address is valid for the identity provider.`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/addressing/src/formatMailLike.ts: -------------------------------------------------------------------------------- 1 | export function formatMailLike(username: string, ...otherParts: string[]): string { 2 | if (otherParts.length === 0) return username; 3 | return `${username}@${otherParts.join('.')}`; 4 | } 5 | -------------------------------------------------------------------------------- /packages/addressing/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addressComparison'; 2 | export { formatAddress } from './addressFormatting'; 3 | export * from './addressFromPublicKey'; 4 | export * from './addressPredicates'; 5 | export * from './checkAddressForErrors'; 6 | export * from './encoding'; 7 | export * from './errors'; 8 | export * from './test.constants'; 9 | export * from './parseWalletAddress'; 10 | export * from './protocols'; 11 | export * from './nameServiceAddress'; 12 | export type { NameServiceAddress as MailchainAddress } from './nameServiceAddress'; 13 | export * from './walletAddress'; 14 | -------------------------------------------------------------------------------- /packages/addressing/src/nameservices/index.ts: -------------------------------------------------------------------------------- 1 | export { NAMESERVICE_DESCRIPTIONS } from './nameserviceDescriptions'; 2 | export { matchesNameservice } from './matchesNameservice'; 3 | -------------------------------------------------------------------------------- /packages/addressing/src/nameservices/matchesNameservice.ts: -------------------------------------------------------------------------------- 1 | import { NameServiceAddress } from '../nameServiceAddress'; 2 | import { NameserviceDescription } from './nameserviceDescriptions'; 3 | 4 | export function matchesNameservice( 5 | address: NameServiceAddress, 6 | nameserviceDescription: NameserviceDescription, 7 | ): string | undefined { 8 | const domainParts = address.domain.split('.'); 9 | if (domainParts[0] !== nameserviceDescription.name) return undefined; 10 | 11 | const usernameParts = address.username.split('.'); 12 | return nameserviceDescription.domains.find((domain) => usernameParts[usernameParts.length - 1] === domain); 13 | } 14 | -------------------------------------------------------------------------------- /packages/addressing/src/parseWalletAddress.ts: -------------------------------------------------------------------------------- 1 | import { NameServiceAddress } from './nameServiceAddress'; 2 | import { ALL_PROTOCOLS, MAILCHAIN, ProtocolType } from './protocols'; 3 | 4 | export function parseWalletAddress( 5 | address: NameServiceAddress, 6 | ): { protocol: ProtocolType; network?: string } | undefined { 7 | const domainParts = address.domain.split('.'); 8 | if (domainParts.length <= 2) return undefined; 9 | 10 | const protocol = ALL_PROTOCOLS.find((search) => domainParts[0] == search); 11 | if (protocol == null || protocol === MAILCHAIN) return undefined; 12 | 13 | const protocolIndex = domainParts.indexOf(protocol); 14 | const network = domainParts[protocolIndex - 1]; 15 | 16 | return { protocol, network }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/consts.ts: -------------------------------------------------------------------------------- 1 | // WARNING: CAREFUL WHEN CHANGING THE VALUES OF THIS CONSTANTS, THEY MIGHT BE ALREADY LINKED TO KEY PATHS 2 | 3 | /** 4 | * Algorand protocol name. 5 | */ 6 | export const ALGORAND = 'algorand' as const; 7 | /** 8 | * Ethereum protocol name. 9 | */ 10 | export const ETHEREUM = 'ethereum' as const; 11 | /** 12 | * Substrate protocol name. 13 | */ 14 | export const SUBSTRATE = 'substrate' as const; 15 | 16 | export const NEAR = 'near' as const; 17 | 18 | export const TEZOS = 'tezos' as const; 19 | 20 | export const SOLANA = 'solana' as const; 21 | 22 | /** 23 | * Mailchain protocol name. 24 | */ 25 | export const MAILCHAIN = 'mailchain' as const; 26 | 27 | export const ALL_PROTOCOLS = [ALGORAND, ETHEREUM, SUBSTRATE, NEAR, MAILCHAIN, TEZOS, SOLANA] as const; 28 | export type ProtocolType = (typeof ALL_PROTOCOLS)[number]; 29 | 30 | const ENABLED_PROTOCOLS = [ETHEREUM, NEAR, TEZOS, SOLANA] as const; 31 | export type EnabledBlockchainProtocol = (typeof ENABLED_PROTOCOLS)[number]; 32 | export function isBlockchainProtocolEnabled(protocol: string): protocol is EnabledBlockchainProtocol { 33 | return ENABLED_PROTOCOLS.includes(protocol as EnabledBlockchainProtocol); 34 | } 35 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/errors.ts: -------------------------------------------------------------------------------- 1 | export class ProtocolNotSupportedError extends Error { 2 | readonly type = 'protocol_unsupported'; 3 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#protocol_unsupported'; 4 | constructor(public readonly protocol: string) { 5 | super(`[${protocol}] is an unsupported protocol. Try again with a different protocol.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/ethereum/address.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { AliceSECP256K1PublicKey, BobSECP256K1PublicKey } from '@mailchain/crypto/secp256k1/test.const'; 3 | import { AliceSECP256K1PublicAddress, BobSECP256K1PublicAddress } from './test.const'; 4 | import { addressFromPublicKey } from './address'; 5 | 6 | describe('addressFromPublicKey', () => { 7 | const tests = [ 8 | { 9 | name: 'alice', 10 | publicKey: AliceSECP256K1PublicKey, 11 | expected: AliceSECP256K1PublicAddress, 12 | shouldThrow: false, 13 | }, 14 | { 15 | name: 'bob', 16 | publicKey: BobSECP256K1PublicKey, 17 | expected: BobSECP256K1PublicAddress, 18 | shouldThrow: false, 19 | }, 20 | { 21 | name: 'incorrect key', 22 | publicKey: AliceED25519PublicKey, 23 | shouldThrow: true, 24 | }, 25 | ]; 26 | test.each(tests)('$name', async (test) => { 27 | if (test.shouldThrow) { 28 | expect(async () => { 29 | await addressFromPublicKey(test.publicKey); 30 | }).rejects.toThrow(); 31 | } else { 32 | expect(await addressFromPublicKey(test.publicKey)).toEqual(test.expected); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/ethereum/address.ts: -------------------------------------------------------------------------------- 1 | import { KindSECP256K1, PublicKey } from '@mailchain/crypto'; 2 | import { decodeHexZeroX } from '@mailchain/encoding'; 3 | 4 | /** 5 | * Derive the ethereum address corresponding to the {@link PublicKey}. 6 | * 7 | * @param publicKey must be with with {@link KindSECP256K1} curve 8 | * @throw if the provided key is on unsupported curve 9 | */ 10 | export async function addressFromPublicKey(publicKey: PublicKey): Promise { 11 | if (publicKey.curve !== KindSECP256K1) { 12 | throw new Error(`public key must be ${KindSECP256K1}`); 13 | } 14 | const { computeAddress } = await import('@ethersproject/transactions'); 15 | 16 | return decodeHexZeroX(computeAddress(publicKey.bytes)); 17 | } 18 | 19 | export function validateEthereumAddress(address: string): boolean { 20 | return /^0x[a-fA-F0-9]{40}/.test(address); 21 | } 22 | 23 | export function validateEthereumTokenOwnerAddress(address: string): boolean { 24 | return /^[0-9]*.0x[a-fA-F0-9]{40}/.test(address); 25 | } 26 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/ethereum/test.const.ts: -------------------------------------------------------------------------------- 1 | import { decodeHexZeroX } from '@mailchain/encoding'; 2 | 3 | export const BobSECP256K1PublicAddressStr = '0x92d8f10248c6a3953cc3692a894655ad05d61efb'; 4 | export const BobSECP256K1PublicAddress = decodeHexZeroX(BobSECP256K1PublicAddressStr); 5 | 6 | export const AliceSECP256K1PublicAddressStr = '0xD5ab4CE3605Cd590Db609b6b5C8901fdB2ef7FE6'; 7 | export const AliceSECP256K1PublicAddress = decodeHexZeroX(AliceSECP256K1PublicAddressStr); 8 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/filecoin/address.ts: -------------------------------------------------------------------------------- 1 | import { isBase32 } from '@mailchain/encoding'; 2 | import { FILECOIN_PREFIX_F4_ETHEREUM, FILECOIN_PREFIX_T4_ETHEREUM } from './const'; 3 | import { convertFilDelegatedAddressToEthAddress } from './delegatedAddress'; 4 | 5 | export function validateFilecoinAddress(address: string): boolean { 6 | if (address.length < 42 && address.length > 49) { 7 | return false; 8 | } else if (!address.startsWith(FILECOIN_PREFIX_F4_ETHEREUM) && !address.startsWith(FILECOIN_PREFIX_T4_ETHEREUM)) { 9 | return false; 10 | } else if (!isBase32(address.slice(4))) { 11 | return false; 12 | } 13 | 14 | return convertFilDelegatedAddressToEthAddress(address).error === undefined; 15 | } 16 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/filecoin/const.ts: -------------------------------------------------------------------------------- 1 | import { SECP256K1PrivateKey } from '@mailchain/crypto'; 2 | import { decodeHex } from '@mailchain/encoding'; 3 | 4 | // Alice 5 | export const AliceFilStr = 'f410faiikgixcdd7alrsbpunjlen4etkcarbfbghn7wi'; 6 | export const AliceFilEthAddressStr = '0x0210a322e218fe05c6417d1a9591bc24d4204425'; 7 | export const AliceFilEthPrivateKeyStr = '6c115551accbd74b46401a531af1dd0890034c18137dddab52a53c626e75b051'; 8 | export const AliceFilEthPrivateKey = new SECP256K1PrivateKey(decodeHex(AliceFilEthPrivateKeyStr)); 9 | 10 | // Bob 11 | export const BobFilAddressStr = 'f410f5blfdi7tmto6o2aa2up4vwtdm7qcnaiqpeazuwy'; 12 | export const BobFilEthAddressStr = '0xe85651a3f364dde76800d51fcada6367e0268110'; 13 | export const BobFilEthPrivateKeyStr = '2c6b29d5545e5804b7fb7e65404782e57dddea84cbcec9b2586add6403808965'; 14 | export const BobFilEthPrivateKey = new SECP256K1PrivateKey(decodeHex(BobFilEthPrivateKeyStr)); 15 | 16 | export const FILECOIN_PREFIX_F4_ETHEREUM = 'f410'; 17 | export const FILECOIN_PREFIX_T4_ETHEREUM = 't410'; 18 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/filecoin/types.ts: -------------------------------------------------------------------------------- 1 | export enum FilecoinAddressType { 2 | SECP256K1 = 1, 3 | BLS12_381 = 3, 4 | DELEGATED = 4, 5 | } 6 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts'; 2 | export * from './errors'; 3 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/near/address.ts: -------------------------------------------------------------------------------- 1 | import { isHex } from '@mailchain/encoding'; 2 | 3 | export function validateNearAccountId(accountId: string) { 4 | return validateNearImplicitAccount(accountId) || validateNearNamedAccount(accountId); 5 | } 6 | 7 | export function validateNearImplicitAccount(address: string) { 8 | return address.length == 64 && isHex(address); 9 | } 10 | 11 | export function validateNearNamedAccount(address: string) { 12 | return ( 13 | address.length >= 2 && 14 | address.length <= 64 && 15 | /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/.test(address) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/solana/address.ts: -------------------------------------------------------------------------------- 1 | import { KindED25519, PublicKey } from '@mailchain/crypto'; 2 | import { decodeBase58 } from '@mailchain/encoding'; 3 | 4 | export function validateSolanaAddress(addressID: string) { 5 | try { 6 | const address = decodeBase58(addressID); 7 | return address.length === 32; 8 | } catch (e) { 9 | return false; 10 | } 11 | } 12 | 13 | export function solanaAddressFromPublicKey(publicKey: PublicKey): Uint8Array { 14 | if (publicKey.curve !== KindED25519) throw new Error('Only ED25519 is supported for Solana'); 15 | if (publicKey.bytes.length !== 32) throw new Error('Invalid public key length for Solana'); 16 | return publicKey.bytes; 17 | } 18 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/solana/test.const.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase58 } from '@mailchain/encoding'; 2 | 3 | export const BobSolanaPublicAddressStr = '47L935B9UmtJ9oeF8Xy7AE1qGnxEPnk42eMtwoeVnsx3'; 4 | export const BobSolanaPublicAddress = decodeBase58(BobSolanaPublicAddressStr); 5 | 6 | export const AliceSolanaPublicAddressStr = '8gw8X5R9c8BvmAR83Z72GANcmkfskXATP3DeyxZkk2U8'; 7 | export const AliceSolanaPublicAddress = decodeBase58(AliceSolanaPublicAddressStr); 8 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/tezos/const.ts: -------------------------------------------------------------------------------- 1 | export enum Prefix { 2 | TZ1 = 'tz1', 3 | TZ2 = 'tz2', 4 | TZ3 = 'tz3', 5 | KT = 'KT', 6 | KT1 = 'KT1', 7 | 8 | EDSK2 = 'edsk2', 9 | SPSK = 'spsk', 10 | P2SK = 'p2sk', 11 | 12 | EDPK = 'edpk', 13 | SPPK = 'sppk', 14 | P2PK = 'p2pk', 15 | 16 | EDESK = 'edesk', 17 | SPESK = 'spesk', 18 | P2ESK = 'p2esk', 19 | 20 | EDSK = 'edsk', 21 | EDSIG = 'edsig', 22 | SPSIG = 'spsig', 23 | P2SIG = 'p2sig', 24 | SIG = 'sig', 25 | EXPR = 'expr', 26 | } 27 | 28 | export const prefix = { 29 | [Prefix.TZ1]: new Uint8Array([6, 161, 159]), 30 | [Prefix.TZ2]: new Uint8Array([6, 161, 161]), 31 | [Prefix.TZ3]: new Uint8Array([6, 161, 164]), 32 | [Prefix.KT]: new Uint8Array([2, 90, 121]), 33 | [Prefix.KT1]: new Uint8Array([2, 90, 121]), 34 | 35 | [Prefix.EDSK]: new Uint8Array([43, 246, 78, 7]), 36 | [Prefix.EDSK2]: new Uint8Array([13, 15, 58, 7]), 37 | [Prefix.SPSK]: new Uint8Array([17, 162, 224, 201]), 38 | [Prefix.P2SK]: new Uint8Array([16, 81, 238, 189]), 39 | 40 | [Prefix.EDPK]: new Uint8Array([13, 15, 37, 217]), 41 | [Prefix.SPPK]: new Uint8Array([3, 254, 226, 86]), 42 | [Prefix.P2PK]: new Uint8Array([3, 178, 139, 127]), 43 | 44 | [Prefix.EDESK]: new Uint8Array([7, 90, 60, 179, 41]), 45 | [Prefix.SPESK]: new Uint8Array([0x09, 0xed, 0xf1, 0xae, 0x96]), 46 | [Prefix.P2ESK]: new Uint8Array([0x09, 0x30, 0x39, 0x73, 0xab]), 47 | 48 | [Prefix.EDSIG]: new Uint8Array([9, 245, 205, 134, 18]), 49 | [Prefix.SPSIG]: new Uint8Array([13, 115, 101, 19, 63]), 50 | [Prefix.P2SIG]: new Uint8Array([54, 240, 44, 52]), 51 | [Prefix.SIG]: new Uint8Array([4, 130, 43]), 52 | [Prefix.EXPR]: new Uint8Array([13, 44, 64, 27]), 53 | }; 54 | -------------------------------------------------------------------------------- /packages/addressing/src/protocols/tezos/test.const.ts: -------------------------------------------------------------------------------- 1 | // AliceED25519PublicKey 2 | export const AliceED25519TzPublicKey = 'edpkuWXy3DeVKmrwvSa9iYRHuCbkL8YeLiXWD2EHnCfVBF3E6B3CP9'; 3 | export const AliceTz1AddressStr = 'tz1cxdX7rUDr4G1LcHH2kVNLzEXBo7va15eV'; 4 | // BobED25519PublicKey 5 | export const BobED25519TzPublicKey = 'edpktzZxD34kwnRNFFAnYTsPJp17ycS4RQbZ1Y7yyXrUyB3rgPhVUL'; 6 | export const BobTz1AddressStr = 'tz1NCxL77QLtoPHjdCx75v7LcjfBtTgZAPv5'; 7 | 8 | // AliceSECP256K1PublicKey 9 | export const AliceSECP256K1TzPublicKey = 'sppk7a7FbTvhn3Qxh5WpXVdNK88xsdpWAZF2ux2RNmvikKFtXYdHdMy'; 10 | export const AliceTz2AddressStr = 'tz2ToZF5fjmcawoUQUa6NoV3jsRmmZNwuEyZ'; 11 | // BobSECP256K1PublicKey 12 | export const BobSECP256K1TzPublicKey = 'sppk7ch3Rj9KwqQHFWoUX93ZVaqxEDJ9z5X4rhd4gDTb4e36cqUY4PW'; 13 | export const BobTz2AddressStr = 'tz29g81yQuHQdXrwtHwbfW6UnNHwfXyfetzZ'; 14 | 15 | // AliceSECP256R1PublicKey 16 | export const AliceSECP256R1TzPublicKey = 'p2pk66tTYL5EvahKAXncbtbRPBkAnxo3CszzUho5wPCgWauBMyvybuB'; 17 | export const AliceTz3AddressStr = 'tz3Lfm6CyfSTZ7EgMckptZZGiPxzs9GK59At'; 18 | // BobSECP256R1PublicKey 19 | export const BobSECP256R1TzPublicKey = 'p2pk66s1R6D2K6dQUPvLtzaQRitR8T8GCinUbsVZrwD3vmU5L3xE3PE'; 20 | export const BobTz3AddressStr = 'tz3fUhqnq67nXVL8wnk2G5xttXMJYF6qrEE5'; 21 | -------------------------------------------------------------------------------- /packages/addressing/src/test.constants.ts: -------------------------------------------------------------------------------- 1 | export const EthereumAlice = new Uint8Array([ 2 | 0xd5, 0xab, 0x4c, 0xe3, 0x60, 0x5c, 0xd5, 0x90, 0xdb, 0x60, 0x9b, 0x6b, 0x5c, 0x89, 0x1, 0xfd, 0xb2, 0xef, 0x7f, 3 | 0xe6, 4 | ]); 5 | 6 | export const EthereumBob = new Uint8Array([ 7 | 0x92, 0xd8, 0xf1, 0x2, 0x48, 0xc6, 0xa3, 0x95, 0x3c, 0xc3, 0x69, 0x2a, 0x89, 0x46, 0x55, 0xad, 0x5, 0xd6, 0x1e, 8 | 0xfb, 9 | ]); 10 | -------------------------------------------------------------------------------- /packages/addressing/src/walletAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { ETHEREUM, MAILCHAIN, ProtocolType } from './protocols'; 2 | import { createWalletAddress, MailchainAddress } from '.'; 3 | 4 | const createWalletAddressTests: { 5 | testName: string; 6 | params: Parameters; 7 | expected: MailchainAddress; 8 | }[] = [ 9 | { 10 | testName: 'simple mailchain', 11 | params: ['alice', MAILCHAIN, 'mailchain.test'], 12 | expected: { username: 'alice', domain: 'mailchain.test' }, 13 | }, 14 | { 15 | testName: 'value mixed case', 16 | params: ['AlIcE', MAILCHAIN, 'mailchain.test'], 17 | expected: { username: 'alice', domain: 'mailchain.test' }, 18 | }, 19 | { 20 | testName: 'value and protocol mixed case', 21 | params: ['AlIcE', 'MaIlChAiN' as ProtocolType, 'mailchain.test'], 22 | expected: { username: 'alice', domain: 'mailchain.test' }, 23 | }, 24 | { 25 | testName: 'domain mixed case', 26 | params: ['alice', MAILCHAIN, 'MaIlChAiN.TeSt'], 27 | expected: { username: 'alice', domain: 'mailchain.test' }, 28 | }, 29 | { 30 | testName: 'ethereum address', 31 | params: ['0xdDfFC3003797e44FCd103eE7A4aE78Ed02853A55', ETHEREUM, 'mailchain.test'], 32 | expected: { username: '0xddffc3003797e44fcd103ee7a4ae78ed02853a55', domain: 'ethereum.mailchain.test' }, 33 | }, 34 | ]; 35 | 36 | test.each(createWalletAddressTests)('$testName', ({ params, expected }) => { 37 | expect(createWalletAddress(...params)).toEqual(expected); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/addressing/src/walletAddress.ts: -------------------------------------------------------------------------------- 1 | import { casingByProtocol } from './addressCasing'; 2 | import { createNameServiceAddress, NameServiceAddress as MailchainAddress } from './nameServiceAddress'; 3 | import { MAILCHAIN, ProtocolType } from './protocols'; 4 | 5 | /** 6 | * Helper method for creating of {@link MailchainAddress} for when the address is with specific protocol. 7 | * It provides the correct casing for the username. 8 | */ 9 | export function createWalletAddress( 10 | username: string, 11 | protocol: ProtocolType, 12 | mailchainAddressDomain: string, 13 | ): MailchainAddress { 14 | protocol = protocol.toLowerCase() as ProtocolType; 15 | const casedUsername = casingByProtocol(username, protocol); 16 | if (protocol === MAILCHAIN) return createNameServiceAddress(casedUsername, mailchainAddressDomain); 17 | return createNameServiceAddress(casedUsername, protocol, mailchainAddressDomain); 18 | } 19 | -------------------------------------------------------------------------------- /packages/addressing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/.gitignore: -------------------------------------------------------------------------------- 1 | src/api 2 | -------------------------------------------------------------------------------- /packages/api/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain API 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/api 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/api 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The signatures package is used by `@mailchain/sdk` to communicate to Mailchain's protocol and services. 24 | 25 | **Note: The API package is not intended to be used directly. Instead it should be used via the SDK as the majority of Mailchain's API's are authenticated. Requests need to be encrypted and signed before sending to across the wire. Response need to be decrypted and or verified before being used.** 26 | -------------------------------------------------------------------------------- /packages/api/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/api/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/api", 3 | "version": "0.31.0", 4 | "description": "Mailchain api tools", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "api" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@mailchain/crypto": "0.31.0", 21 | "@mailchain/encoding": "0.31.0", 22 | "@noble/hashes": "^1.3.0", 23 | "axios": "1.6.0", 24 | "canonicalize": "^1.0.8", 25 | "lodash": "^4.17.21" 26 | }, 27 | "devDependencies": { 28 | "@mailchain/keyring": "0.31.0", 29 | "axios-mock-adapter": "^1.21.1" 30 | }, 31 | "scripts": { 32 | "build": "run -T tsup", 33 | "test": "run -T jest", 34 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 35 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/api/src/axios/axios.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithPublicKey } from '@mailchain/crypto'; 2 | import axios, { AxiosInstance } from 'axios'; 3 | import { encodeBase64UrlSafe } from '@mailchain/encoding'; 4 | import { signJWT } from '../jwt'; 5 | import { createTokenPayload } from './token'; 6 | 7 | export const getAxiosWithSigner = (requestKey: SignerWithPublicKey): AxiosInstance => { 8 | const axiosInstance = axios.create(); 9 | axiosInstance.interceptors.request.use(async (request) => { 10 | if (request.headers) { 11 | const expires = Math.floor(Date.now() / 1000 + 60 * 5); // 5 mins 12 | const tokenPayload = createTokenPayload( 13 | new URL(request?.url ?? ''), 14 | request.method?.toUpperCase() ?? '', 15 | request.data, 16 | expires, 17 | ); 18 | const token = await signJWT(requestKey, tokenPayload); 19 | request.headers.Authorization = `vapid t=${token}, k=${encodeBase64UrlSafe(requestKey.publicKey.bytes)}`; 20 | } 21 | return request; 22 | }); 23 | 24 | return axiosInstance; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/api/src/axios/config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration as APIConfiguration } from '../api/configuration'; 2 | 3 | export function createAxiosConfiguration(apiPath: string): APIConfiguration { 4 | return new APIConfiguration({ 5 | basePath: apiPath, 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /packages/api/src/axios/index.ts: -------------------------------------------------------------------------------- 1 | export * from './axios'; 2 | export * from './config'; 3 | -------------------------------------------------------------------------------- /packages/api/src/helpers/address.ts: -------------------------------------------------------------------------------- 1 | import { decode } from '@mailchain/encoding'; 2 | import { Address } from '../api'; 3 | 4 | export const getAddressFromApiResponse = (address: Address) => { 5 | return decode(address.encoding, address.value); 6 | }; 7 | -------------------------------------------------------------------------------- /packages/api/src/helpers/apiKeyToCryptoKey.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrivateKey, 3 | PublicKey, 4 | ED25519PrivateKey, 5 | ED25519PublicKey, 6 | SECP256K1PublicKey, 7 | SECP256K1PrivateKey, 8 | } from '@mailchain/crypto'; 9 | import { decode } from '@mailchain/encoding'; 10 | import { SECP256R1PrivateKey, SECP256R1PublicKey } from '@mailchain/crypto/secp256r1'; 11 | import { 12 | PrivateKey as ApiPrivateKey, 13 | PrivateKeyCurveEnum, 14 | PublicKey as ApiPublicKey, 15 | PublicKeyCurveEnum, 16 | } from '../api/api'; 17 | import { ErrorUnsupportedKey } from './errors'; 18 | 19 | /** Convert {@link ApiPublicKey} to {@link PublicKey} */ 20 | export function convertPublic(key: ApiPublicKey): PublicKey { 21 | switch (key.curve) { 22 | case PublicKeyCurveEnum.Ed25519: 23 | return new ED25519PublicKey(decode(key.encoding, key.value)); 24 | case PublicKeyCurveEnum.Secp256k1: 25 | return new SECP256K1PublicKey(decode(key.encoding, key.value)); 26 | case PublicKeyCurveEnum.Secp256r1: 27 | return new SECP256R1PublicKey(decode(key.encoding, key.value)); 28 | default: 29 | throw new ErrorUnsupportedKey(key.curve); 30 | } 31 | } 32 | 33 | /** Convert {@link ApiPrivateKey} to {@link PrivateKey} */ 34 | export function convertPrivate(key: ApiPrivateKey): PrivateKey { 35 | switch (key.curve) { 36 | case PrivateKeyCurveEnum.Ed25519: 37 | return ED25519PrivateKey.fromSecretKey(decode(key.encoding, key.value)); 38 | case PrivateKeyCurveEnum.Secp256k1: 39 | return new SECP256K1PrivateKey(decode(key.encoding, key.value)); 40 | case PrivateKeyCurveEnum.Secp256r1: 41 | return new SECP256R1PrivateKey(decode(key.encoding, key.value)); 42 | default: 43 | throw new ErrorUnsupportedKey(key.curve); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/api/src/helpers/cryptoKeyToApiKey.ts: -------------------------------------------------------------------------------- 1 | import { KindED25519, KindSECP256K1, KindSECP256R1, PrivateKey, PublicKey } from '@mailchain/crypto'; 2 | import { encodeHexZeroX } from '@mailchain/encoding'; 3 | import { 4 | PublicKey as ApiPublicKey, 5 | PrivateKey as ApiPrivateKey, 6 | PublicKeyCurveEnum, 7 | PrivateKeyCurveEnum, 8 | } from '../api/api'; 9 | import { ErrorUnsupportedKey } from './errors'; 10 | 11 | /** Convert {@link PublicKey} to {@link ApiPublicKey}. */ 12 | export function convertPublic(key: PublicKey): ApiPublicKey { 13 | switch (key.curve) { 14 | case KindED25519: 15 | return { curve: PublicKeyCurveEnum.Ed25519, value: encodeHexZeroX(key.bytes), encoding: 'hex/0x-prefix' }; 16 | case KindSECP256K1: 17 | return { curve: PublicKeyCurveEnum.Secp256k1, value: encodeHexZeroX(key.bytes), encoding: 'hex/0x-prefix' }; 18 | case KindSECP256R1: 19 | return { curve: PublicKeyCurveEnum.Secp256r1, value: encodeHexZeroX(key.bytes), encoding: 'hex/0x-prefix' }; 20 | default: 21 | throw new ErrorUnsupportedKey(key.curve); 22 | } 23 | } 24 | 25 | /** Convert {@link PrivateKey} to {@link ApiPrivateKey} */ 26 | export function convertPrivate(key: PrivateKey): ApiPrivateKey { 27 | switch (key.curve) { 28 | case KindED25519: 29 | return { curve: PrivateKeyCurveEnum.Ed25519, value: encodeHexZeroX(key.bytes), encoding: 'hex/0x-prefix' }; 30 | case KindSECP256K1: 31 | return { 32 | curve: PrivateKeyCurveEnum.Secp256k1, 33 | value: encodeHexZeroX(key.bytes), 34 | encoding: 'hex/0x-prefix', 35 | }; 36 | case KindSECP256R1: 37 | return { 38 | curve: PrivateKeyCurveEnum.Secp256r1, 39 | value: encodeHexZeroX(key.bytes), 40 | encoding: 'hex/0x-prefix', 41 | }; 42 | default: 43 | throw new ErrorUnsupportedKey(key.curve); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/api/src/helpers/encoding.ts: -------------------------------------------------------------------------------- 1 | import { EncodingType, EncodingTypes } from '@mailchain/encoding'; 2 | import { AddressEncodingEnum } from '../api/api'; 3 | 4 | export function encodingTypeToEncodingEnum(encoding: EncodingType): AddressEncodingEnum { 5 | switch (encoding) { 6 | case EncodingTypes.Hex: 7 | return AddressEncodingEnum.HexPlain; 8 | case EncodingTypes.Hex0xPrefix: 9 | return AddressEncodingEnum.Hex0xPrefix; 10 | case EncodingTypes.Utf8: 11 | return AddressEncodingEnum.TextUtf8; 12 | case EncodingTypes.Base58: 13 | return AddressEncodingEnum.Base58Plain; 14 | } 15 | throw new Error(`unsupported encoding by API of [${encoding}]`); 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/helpers/errors.ts: -------------------------------------------------------------------------------- 1 | export class ErrorUnsupportedKey extends Error { 2 | constructor(curve: string) { 3 | super(`${curve} is not supported`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/api/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as apiKeyToCryptoKey from './apiKeyToCryptoKey'; 2 | import * as cryptoKeyToApiKey from './cryptoKeyToApiKey'; 3 | 4 | const ApiKeyConvert = { 5 | private: apiKeyToCryptoKey.convertPrivate, 6 | public: apiKeyToCryptoKey.convertPublic, 7 | } as const; 8 | const CryptoKeyConvert = { 9 | public: cryptoKeyToApiKey.convertPublic, 10 | private: cryptoKeyToApiKey.convertPrivate, 11 | } as const; 12 | 13 | export { ApiKeyConvert, CryptoKeyConvert }; 14 | export * from './encoding'; 15 | export * from './address'; 16 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './jwt'; 3 | export * from './axios'; 4 | export { createAxiosConfiguration } from './axios/config'; 5 | export { ApiKeyConvert, CryptoKeyConvert, encodingTypeToEncodingEnum, getAddressFromApiResponse } from './helpers'; 6 | -------------------------------------------------------------------------------- /packages/api/src/jwt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt'; 2 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/crypto/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Crypto 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/crypto 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/crypto 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The crypto package is used by `@mailchain/sdk` to support securely registering blockchain addresses and sending messages via Mailchain. 24 | -------------------------------------------------------------------------------- /packages/crypto/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/crypto/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/crypto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/crypto", 3 | "version": "0.31.0", 4 | "license": "Apache-2.0", 5 | "dependencies": { 6 | "@ethersproject/hash": "5.7.0", 7 | "@ethersproject/signing-key": "5.6.2", 8 | "@mailchain/encoding": "0.31.0", 9 | "@noble/curves": "^0.8.2", 10 | "@noble/hashes": "^1.3.0", 11 | "@scure/bip39": "^1.2.0", 12 | "bn.js": "^5.2.1", 13 | "ed2curve": "^0.3.0", 14 | "secp256k1": "^4.0.2", 15 | "tweetnacl": "^1.0.3" 16 | }, 17 | "devDependencies": { 18 | "@types/bn.js": "^5.1.0", 19 | "@types/ed2curve": "^0.2.3", 20 | "@types/secp256k1": "^4.0.2" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 25 | "directory": "crypto" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 29 | }, 30 | "engines": { 31 | "node": ">=15.0.0" 32 | }, 33 | "scripts": { 34 | "build": "run -T tsup", 35 | "test": "run -T jest", 36 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 37 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/cipher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NaCl with Elliptic Curve Diffie–Hellman key exchange 3 | */ 4 | export const NACLECDH = 0x2a; 5 | /** 6 | * NaCl with a secret key 7 | */ 8 | export const NACLSK = 0x2b; 9 | 10 | export const KindNaClSecretKey = 'nacl-secret-key'; 11 | 12 | export type EncryptedContent = Uint8Array; 13 | 14 | export type PlainContent = Uint8Array; 15 | 16 | export interface Decrypter { 17 | decrypt: (input: EncryptedContent) => Promise; 18 | } 19 | 20 | export interface Encrypter { 21 | encrypt: (input: PlainContent) => Promise; 22 | } 23 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/ecdh/ecdh.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PublicKey } from '../../ed25519/test.const'; 2 | import { AliceUnknownPublicKey } from '../../testing/public'; 3 | import { ED25519KeyExchange } from './ed25519'; 4 | import { fromPublicKey } from './'; 5 | 6 | describe('FromPublicKey', () => { 7 | const tests = [ 8 | { 9 | name: 'creates-ed25519-key-exchange', 10 | key: AliceED25519PublicKey, 11 | expected: ED25519KeyExchange, 12 | shouldThrow: false, 13 | }, 14 | { 15 | name: 'fails-to-creates-with-unknown-key', 16 | key: AliceUnknownPublicKey, 17 | expected: null, 18 | shouldThrow: true, 19 | }, 20 | ]; 21 | test.each(tests)('$name', async (test) => { 22 | if (test.shouldThrow) { 23 | expect(() => { 24 | fromPublicKey(test.key); 25 | }).toThrow(); 26 | } else { 27 | expect(fromPublicKey(test.key).constructor).toEqual(test.expected); 28 | } 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/ecdh/ecdh.ts: -------------------------------------------------------------------------------- 1 | import { KeyExchange } from '..'; 2 | import { RandomFunction, secureRandom } from '../../rand'; 3 | import { PublicKey } from '../../public'; 4 | import { PrivateKey } from '../../private'; 5 | import { ED25519PublicKey } from '../../ed25519/public'; 6 | import { ED25519PrivateKey } from '../../ed25519/private'; 7 | import { ED25519KeyExchange } from './ed25519'; 8 | 9 | export function fromPublicKey(publicKey: PublicKey, rand: RandomFunction = secureRandom): KeyExchange { 10 | switch (publicKey.constructor) { 11 | case ED25519PublicKey: 12 | return new ED25519KeyExchange(rand); 13 | default: 14 | throw RangeError('unknown public key type'); 15 | } 16 | } 17 | 18 | export function fromPrivateKey(privateKey: PrivateKey, rand: RandomFunction = secureRandom): KeyExchange { 19 | switch (privateKey.constructor) { 20 | case ED25519PrivateKey: 21 | return new ED25519KeyExchange(rand); 22 | default: 23 | throw RangeError('unknown private key type'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/ecdh/ed25519.ts: -------------------------------------------------------------------------------- 1 | import nacl from 'tweetnacl'; 2 | import ed2curve from 'ed2curve'; 3 | import { KeyExchange } from '../'; 4 | import { RandomFunction, secureRandom } from '../../rand'; 5 | import { PublicKey } from '../../public'; 6 | import { PrivateKey } from '../../private'; 7 | import { ED25519PublicKey } from '../../ed25519/public'; 8 | import { ED25519PrivateKey, asED25519PrivateKey } from '../../ed25519/private'; 9 | 10 | export class ED25519KeyExchange implements KeyExchange { 11 | randomFunc: RandomFunction; 12 | constructor(randomFunc: RandomFunction = secureRandom) { 13 | this.randomFunc = randomFunc; 14 | } 15 | 16 | async EphemeralKey(): Promise { 17 | return ED25519PrivateKey.generate(this.randomFunc); 18 | } 19 | 20 | async SharedSecret(privateKey: PrivateKey, publicKey: PublicKey): Promise { 21 | if (privateKey.publicKey.bytes.toString() === publicKey.bytes.toString()) { 22 | throw new Error('public key can not be from private key'); 23 | } 24 | 25 | const publicKeyBytes = ED25519KeyExchange.publicKeyToCurve25519(publicKey); 26 | const privateKeyBytes = ED25519KeyExchange.privateKeyToCurve25519(asED25519PrivateKey(privateKey)); 27 | 28 | return nacl.scalarMult(privateKeyBytes, publicKeyBytes); 29 | } 30 | 31 | static privateKeyToCurve25519(privateKey: ED25519PrivateKey): Uint8Array { 32 | return ed2curve.convertSecretKey(privateKey.bytes); 33 | } 34 | 35 | static publicKeyToCurve25519(publicKey: ED25519PublicKey): Uint8Array { 36 | const output = ed2curve.convertPublicKey(publicKey.bytes); 37 | if (output === null) { 38 | throw new Error('invalid public key'); 39 | } 40 | return output; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/ecdh/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ecdh'; 2 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/index.ts: -------------------------------------------------------------------------------- 1 | export { ED25519KeyExchange } from './ecdh/ed25519'; 2 | 3 | export * from './cipher'; 4 | export * from './keyExchange'; 5 | export * from './nacl'; 6 | export * from './ecdh'; 7 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/keyExchange.ts: -------------------------------------------------------------------------------- 1 | import { PrivateKey, PublicKey } from '..'; 2 | 3 | export interface KeyExchange { 4 | EphemeralKey: () => Promise; 5 | 6 | SharedSecret: (privateKey: PrivateKey, publicKey: PublicKey) => Promise; 7 | } 8 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './publicKeyEncrypter'; 2 | export * from './publicKeyDecrypter'; 3 | export * from './privateKeyEncrypter'; 4 | export * from './privateKeyDecrypter'; 5 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/privateKeyDecrypter.ts: -------------------------------------------------------------------------------- 1 | import ed2curve from 'ed2curve'; 2 | import { idFromPrivateKey } from '../../multikey/ids'; 3 | import { EncryptedContent, PlainContent, Decrypter } from '../cipher'; 4 | import { PrivateKey } from '../../private'; 5 | import { SECP256K1PrivateKey } from '../../secp256k1/private'; 6 | import { ED25519PrivateKey } from '../../ed25519/private'; 7 | import { deserializePrivateKeyEncryptedContent } from './serialization'; 8 | import { easyOpen } from './secretbox'; 9 | 10 | export class PrivateKeyDecrypter implements Decrypter { 11 | private _secretKey: Uint8Array; 12 | private _keyId: number; 13 | 14 | constructor(privateKey: PrivateKey) { 15 | this._keyId = idFromPrivateKey(privateKey); 16 | 17 | switch (privateKey.constructor) { 18 | case ED25519PrivateKey: 19 | this._secretKey = ed2curve.convertSecretKey(privateKey.bytes); 20 | break; 21 | case SECP256K1PrivateKey: 22 | this._secretKey = privateKey.bytes; 23 | break; 24 | default: 25 | throw RangeError('unknown private key type'); 26 | } 27 | } 28 | static fromPrivateKey(key: PrivateKey): PrivateKeyDecrypter { 29 | return new this(key); 30 | } 31 | 32 | async decrypt(input: EncryptedContent): Promise { 33 | const secretData = deserializePrivateKeyEncryptedContent(input); 34 | if (this._keyId !== secretData.keyId) { 35 | throw Error('key id does not match supplied key'); 36 | } 37 | 38 | return easyOpen(secretData.encryptedContent, this._secretKey); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/privateKeyEncrypter.ts: -------------------------------------------------------------------------------- 1 | import ed2curve from 'ed2curve'; 2 | import { idFromPrivateKey } from '../../multikey/ids'; 3 | import { EncryptedContent, Encrypter } from '../cipher'; 4 | import { RandomFunction, secureRandom } from '../../rand'; 5 | import { PrivateKey } from '../../private'; 6 | import { SECP256K1PrivateKey } from '../../secp256k1/private'; 7 | import { ED25519PrivateKey } from '../../ed25519/private'; 8 | import { serializePrivateKeyEncryptedContent } from './serialization'; 9 | import { easySeal } from './secretbox'; 10 | 11 | export class PrivateKeyEncrypter implements Encrypter { 12 | private _rand: RandomFunction; 13 | private _secretKey: Uint8Array; 14 | private _keyId: number; 15 | 16 | constructor(privateKey: PrivateKey, rand: RandomFunction = secureRandom) { 17 | this._rand = rand; 18 | this._keyId = idFromPrivateKey(privateKey); 19 | 20 | switch (privateKey.constructor) { 21 | case ED25519PrivateKey: 22 | this._secretKey = ed2curve.convertSecretKey(privateKey.bytes); 23 | break; 24 | case SECP256K1PrivateKey: 25 | this._secretKey = privateKey.bytes; 26 | break; 27 | default: 28 | throw RangeError('unknown private key type'); 29 | } 30 | } 31 | static fromPrivateKey(key: PrivateKey, rand: RandomFunction = secureRandom): PrivateKeyEncrypter { 32 | return new this(key, rand); 33 | } 34 | 35 | async encrypt(input: Uint8Array): Promise { 36 | const sealedBox = easySeal(input, this._secretKey, this._rand); 37 | 38 | return serializePrivateKeyEncryptedContent(sealedBox, this._keyId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/publicKeyDecrypter.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedContent, PlainContent, Decrypter } from '../cipher'; 2 | import { KeyExchange } from '../keyExchange'; 3 | import { PrivateKey } from '../../private'; 4 | import { fromPrivateKey } from '../ecdh/ecdh'; 5 | import { easyOpen } from './secretbox'; 6 | import { deserializePublicKeyEncryptedContent } from './serialization'; 7 | 8 | export class PublicKeyDecrypter implements Decrypter { 9 | private _keyEx: KeyExchange; 10 | private _prvKey: PrivateKey; 11 | 12 | constructor(keyEx: KeyExchange, prvKey: PrivateKey) { 13 | this._keyEx = keyEx; 14 | this._prvKey = prvKey; 15 | } 16 | static FromPrivateKey(key: PrivateKey): PublicKeyDecrypter { 17 | return new this(fromPrivateKey(key), key); 18 | } 19 | 20 | async decrypt(input: EncryptedContent): Promise { 21 | const secretData = deserializePublicKeyEncryptedContent(input); 22 | 23 | const sharedSecret = await this._keyEx.SharedSecret(this._prvKey, secretData.pubKey); 24 | 25 | return easyOpen(secretData.encryptedContent, sharedSecret); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/publicKeyEncrypter.ts: -------------------------------------------------------------------------------- 1 | import { KeyExchange } from '../keyExchange'; 2 | import { EncryptedContent, Encrypter } from '../cipher'; 3 | import { RandomFunction, secureRandom } from '../../rand'; 4 | import { PublicKey } from '../../public'; 5 | import { fromPublicKey } from '../ecdh/ecdh'; 6 | import { easySeal } from './secretbox'; 7 | import { serializePublicKeyEncryptedContent } from './serialization'; 8 | 9 | export class PublicKeyEncrypter implements Encrypter { 10 | private _keyEx: KeyExchange; 11 | private _pubKey: PublicKey; 12 | private _rand: RandomFunction; 13 | 14 | constructor(keyEx: KeyExchange, pubKey: PublicKey, rand: RandomFunction = secureRandom) { 15 | this._rand = rand; 16 | this._keyEx = keyEx; 17 | this._pubKey = pubKey; 18 | } 19 | static FromPublicKey(key: PublicKey): PublicKeyEncrypter { 20 | return new this(fromPublicKey(key), key); 21 | } 22 | 23 | async encrypt(input: Uint8Array): Promise { 24 | const ephemeralPrvKey = await this._keyEx.EphemeralKey(); 25 | const sharedSecret = await this._keyEx.SharedSecret(ephemeralPrvKey, this._pubKey); 26 | const sealedBox = easySeal(input, sharedSecret, this._rand); 27 | 28 | return serializePublicKeyEncryptedContent(sealedBox, ephemeralPrvKey.publicKey); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/publicKeyEndToEnd.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AliceED25519PublicKey, 3 | BobED25519PublicKey, 4 | BobED25519PrivateKey, 5 | AliceED25519PrivateKey, 6 | } from '../../ed25519/test.const'; 7 | import { ED25519KeyExchange } from '../ecdh/ed25519'; 8 | import { PublicKeyDecrypter, PublicKeyEncrypter } from '.'; 9 | 10 | describe('encrypt-then-decrypt', () => { 11 | const tests = [ 12 | { 13 | name: 'ed25519-to-bob', 14 | keyEx: new ED25519KeyExchange(), 15 | recipientPublicKey: BobED25519PublicKey, 16 | recipientPrivateKey: BobED25519PrivateKey, 17 | message: new Uint8Array(Buffer.from('message', 'ascii')), 18 | }, 19 | { 20 | name: 'ed25519-to-alice', 21 | keyEx: new ED25519KeyExchange(), 22 | recipientPublicKey: AliceED25519PublicKey, 23 | recipientPrivateKey: AliceED25519PrivateKey, 24 | message: new Uint8Array(Buffer.from('message', 'ascii')), 25 | }, 26 | ]; 27 | test.each(tests)('%p', async (test) => { 28 | const encrypter = new PublicKeyEncrypter(test.keyEx, test.recipientPublicKey); 29 | const encrypted = await encrypter.encrypt(test.message); 30 | 31 | const decrypter = new PublicKeyDecrypter(test.keyEx, test.recipientPrivateKey); 32 | const decrypted = await decrypter.decrypt(encrypted); 33 | 34 | expect(decrypted).toEqual(test.message); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/crypto/src/cipher/nacl/secretbox.ts: -------------------------------------------------------------------------------- 1 | import nacl from 'tweetnacl'; 2 | import { RandomFunction } from '../../rand'; 3 | 4 | const nonceSize = 24; 5 | const secretKeySize = 32; 6 | 7 | export function easySeal(message: Uint8Array | Buffer, secretKey: Uint8Array, rand: RandomFunction): Uint8Array { 8 | const messagePrivate = message instanceof Buffer ? new Uint8Array(message) : message; // ensure it's a Uint8Array as secretbox checks instance of 9 | 10 | if (secretKey.length !== secretKeySize) { 11 | throw new RangeError('secret key must be 32 bytes'); 12 | } 13 | 14 | const nonce = rand(nonceSize); 15 | 16 | const encrypted = nacl.secretbox(messagePrivate, nonce, secretKey); 17 | const out = new Uint8Array(nonceSize + encrypted.length); 18 | out.set(nonce, 0); 19 | out.set(new Uint8Array(encrypted), nonceSize); 20 | 21 | return out; 22 | } 23 | 24 | export function easyOpen(sealedBox: Uint8Array, secretKey: Uint8Array): Uint8Array { 25 | const sealedBoxPrivate = sealedBox instanceof Buffer ? new Uint8Array(sealedBox) : sealedBox; // ensure it's a Uint8Array as secretbox checks instance of 26 | 27 | if (secretKey.length !== secretKeySize) { 28 | throw new RangeError('secret key must be 32 bytes'); 29 | } 30 | 31 | if (sealedBoxPrivate.length < nonceSize) { 32 | throw new RangeError('secret box must be longer than nonce'); 33 | } 34 | 35 | const nonce = sealedBoxPrivate.slice(0, nonceSize); 36 | const seal = sealedBoxPrivate.slice(nonceSize); 37 | 38 | const ret = nacl.secretbox.open(seal, nonce, secretKey); 39 | if (ret === null) { 40 | throw new Error('secretbox: could not decrypt data with private key'); 41 | } 42 | 43 | return ret; 44 | } 45 | -------------------------------------------------------------------------------- /packages/crypto/src/ed25519/derivation.ts: -------------------------------------------------------------------------------- 1 | import { blake2b } from '@noble/hashes/blake2b'; 2 | import { chainCodeFromDeriveIndex, ExtendedPrivateKey } from '../hd'; 3 | import { asED25519PrivateKey, ED25519PrivateKey } from './private'; 4 | import { ED25519ExtendedPrivateKey } from './hd'; 5 | 6 | // 'Ed25519HDKD' encoded with length prefix 7 | const HDKD = new Uint8Array([44, 69, 100, 50, 53, 53, 49, 57, 72, 68, 75, 68]); 8 | 9 | export function ed25519DeriveHardenedKey( 10 | parentKey: ExtendedPrivateKey, 11 | index: string | number | Uint8Array, 12 | ): ED25519ExtendedPrivateKey { 13 | const chainCode = chainCodeFromDeriveIndex(index); 14 | const seed = asED25519PrivateKey(parentKey.privateKey).bytes.slice(0, 32); 15 | 16 | return ED25519ExtendedPrivateKey.fromPrivateKey( 17 | ED25519PrivateKey.fromSeed( 18 | blake2b(Uint8Array.from([...HDKD, ...seed, ...chainCode]), { 19 | dkLen: 256 / 8, 20 | }), 21 | ), 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/crypto/src/ed25519/hd.test.ts: -------------------------------------------------------------------------------- 1 | import { UnknownPrivateKey } from '../testing'; 2 | import { AliceED25519PrivateKey, BobED25519PrivateKey } from './test.const'; 3 | import { ED25519ExtendedPrivateKey } from '.'; 4 | 5 | describe('FromPrivateKey()', () => { 6 | const tests = [ 7 | { 8 | name: 'alice', 9 | arg: AliceED25519PrivateKey, 10 | expected: { 11 | bytes: AliceED25519PrivateKey.bytes, 12 | privateKey: AliceED25519PrivateKey, 13 | } as ED25519ExtendedPrivateKey, 14 | shouldThrow: false, 15 | }, 16 | { 17 | name: 'bob', 18 | arg: BobED25519PrivateKey, 19 | expected: { 20 | bytes: BobED25519PrivateKey.bytes, 21 | privateKey: BobED25519PrivateKey, 22 | } as ED25519ExtendedPrivateKey, 23 | shouldThrow: false, 24 | }, 25 | { 26 | name: 'invalid', 27 | arg: new UnknownPrivateKey(), 28 | expected: null, 29 | shouldThrow: true, 30 | }, 31 | ]; 32 | test.each(tests)('$name', async (test) => { 33 | if (test.shouldThrow) { 34 | expect(() => { 35 | ED25519ExtendedPrivateKey.fromPrivateKey(test.arg); 36 | }).toThrow(); 37 | } else { 38 | expect(ED25519ExtendedPrivateKey.fromPrivateKey(test.arg)).toEqual(test.expected); 39 | } 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/crypto/src/ed25519/hd.ts: -------------------------------------------------------------------------------- 1 | import { PrivateKey } from '../private'; 2 | import { ExtendedPrivateKey } from '../hd'; 3 | import { asED25519PrivateKey, ED25519PrivateKey } from './private'; 4 | 5 | export class ED25519ExtendedPrivateKey implements ExtendedPrivateKey { 6 | bytes: Uint8Array; 7 | privateKey: ED25519PrivateKey; 8 | 9 | private constructor(privateKey: ED25519PrivateKey) { 10 | this.privateKey = privateKey; 11 | this.bytes = privateKey.bytes; 12 | } 13 | static fromPrivateKey(privateKey: PrivateKey): ED25519ExtendedPrivateKey { 14 | return new this(asED25519PrivateKey(privateKey)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/crypto/src/ed25519/index.ts: -------------------------------------------------------------------------------- 1 | export { ED25519PrivateKey } from './private'; 2 | export { ED25519PublicKey, ED25519PublicKeyLen } from './public'; 3 | export { ED25519ExtendedPrivateKey } from './hd'; 4 | export { ed25519DeriveHardenedKey as deriveHardenedKey } from './derivation'; 5 | -------------------------------------------------------------------------------- /packages/crypto/src/ed25519/public.ts: -------------------------------------------------------------------------------- 1 | import nacl from 'tweetnacl'; 2 | import { KindED25519 } from '../keys'; 3 | import { PublicKey } from '../public'; 4 | 5 | export const ED25519PublicKeyLen = 32; 6 | 7 | export class ED25519PublicKey implements PublicKey { 8 | readonly curve: string = KindED25519; 9 | readonly bytes: Uint8Array; 10 | 11 | constructor(bytes: Uint8Array) { 12 | if (bytes.length !== ED25519PublicKeyLen) { 13 | throw new RangeError('invalid public key length'); 14 | } 15 | this.bytes = bytes; 16 | } 17 | 18 | async verify(message: Uint8Array, sig: Uint8Array): Promise { 19 | return nacl.sign.detached.verify(message, sig, this.bytes); 20 | } 21 | } 22 | 23 | export function asED25519PublicKey(key: PublicKey): ED25519PublicKey { 24 | if (key.constructor !== ED25519PublicKey) { 25 | throw new Error('key must be ed25519'); 26 | } 27 | 28 | return key; 29 | } 30 | -------------------------------------------------------------------------------- /packages/crypto/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ErrorUnsupportedKey extends Error { 2 | constructor(curve: string) { 3 | super(`${curve} is not supported`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/crypto/src/index.ts: -------------------------------------------------------------------------------- 1 | export { ED25519ExtendedPrivateKey, ED25519PrivateKey, ED25519PublicKey, deriveHardenedKey } from './ed25519'; 2 | export { SECP256K1PrivateKey, SECP256K1PublicKey } from './secp256k1'; 3 | export * from './keys'; 4 | export * from './multikey'; 5 | export * from './public'; 6 | export * from './private'; 7 | export * from './rand'; 8 | export * from './hd'; 9 | export * from './cipher'; 10 | export * from './errors'; 11 | -------------------------------------------------------------------------------- /packages/crypto/src/keys.ts: -------------------------------------------------------------------------------- 1 | // KindSECP256K1 string identifier for secp256k1 keys. 2 | export const KindSECP256K1 = 'secp256k1'; 3 | // KindED25519 string identifier for ed25519 keys. 4 | export const KindED25519 = 'ed25519'; 5 | // KindSECP256R1 string identifier for P256 keys. 6 | export const KindSECP256R1 = 'secp256r1'; 7 | // KindSR25519 string identifier for sr25519 keys. 8 | export const KindSR25519 = 'sr25519'; 9 | // KindNoOp string identifier for no-operation keys. 10 | export const KindNoOp = 'noop'; 11 | 12 | // IdSECP256K1 Id identifier for secp256k1 keys. 13 | export const IdSECP256K1 = 0xe1; 14 | // IdED25519 Id identifier for ed25519 keys. 15 | export const IdED25519 = 0xe2; 16 | // IdSR25519 Id identifier for sr25519 keys. 17 | export const IdSR25519 = 0xe3; 18 | // IdSECP256R1 Id identifier for SECP256R1 keys. 19 | export const IdSECP256R1 = 0xe4; 20 | 21 | // IdNonSpecified Id identifier for non specified secret keys. 22 | export const IdNonSpecified = 0xee; 23 | 24 | export type KeyKinds = typeof KindED25519 | typeof KindSECP256K1 | typeof KindSR25519 | typeof KindSECP256R1; 25 | 26 | export enum CurveIds { 27 | SECP256K1 = IdSECP256K1, 28 | ED25519 = IdED25519, 29 | SR25519 = IdSR25519, 30 | SECP256R1 = IdSECP256R1, 31 | } 32 | -------------------------------------------------------------------------------- /packages/crypto/src/mnemonic/mnemonic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | entropyToMnemonic, 3 | mnemonicToEntropy, 4 | generateMnemonic, 5 | validateMnemonic, 6 | mnemonicToSeedSync, 7 | } from '@scure/bip39'; 8 | import { wordlist } from '@scure/bip39/wordlists/english'; 9 | 10 | export function generate(words: 12 | 24 = 24): string { 11 | return generateMnemonic(wordlist, words === 12 ? 128 : 256); 12 | } 13 | 14 | export function toEntropy(mnemonic: string): Uint8Array { 15 | return mnemonicToEntropy(mnemonic, wordlist); 16 | } 17 | 18 | export function fromEntropy(entropy: Uint8Array): string { 19 | return entropyToMnemonic(entropy, wordlist); 20 | } 21 | 22 | export function validate(mnemonic: string): boolean { 23 | return validateMnemonic(mnemonic, wordlist); 24 | } 25 | 26 | export function toSeed(mnemonic: string, password?: string, byteLength?: 32 | 64 | undefined): Uint8Array { 27 | if (!validate(mnemonic)) { 28 | throw new Error('Invalid mnemonic'); 29 | } 30 | return mnemonicToSeedSync(mnemonic, password).slice(0, byteLength === undefined ? 32 : byteLength); 31 | } 32 | -------------------------------------------------------------------------------- /packages/crypto/src/multikey/bytes.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '../public'; 2 | import { PrivateKey } from '../private'; 3 | import { idFromPrivateKey, idFromPublicKey, privateKeyFromId, publicKeyFromId } from './ids'; 4 | 5 | export function publicKeyToBytes(key: PublicKey): Uint8Array { 6 | const out = new Uint8Array(key.bytes.length + 1); 7 | 8 | out[0] = idFromPublicKey(key); 9 | out.set(key.bytes, 1); 10 | 11 | return out; 12 | } 13 | 14 | export function publicKeyFromBytes(bytes: Uint8Array | Buffer): PublicKey { 15 | if (bytes.length < 32 + 1) { 16 | throw Error('public key is too short to contain ID byte'); 17 | } 18 | // create a new Uint8Array object as it bytes might be a buffer 19 | return publicKeyFromId(bytes[0], new Uint8Array(bytes.slice(1))); 20 | } 21 | 22 | export function privateKeyToBytes(key: PrivateKey): Uint8Array { 23 | const out = new Uint8Array(key.bytes.length + 1); 24 | 25 | out[0] = idFromPrivateKey(key); 26 | out.set(key.bytes, 1); 27 | 28 | return out; 29 | } 30 | 31 | export function privateKeyFromBytes(bytes: Uint8Array): PrivateKey { 32 | if (bytes.length < 32 + 1) { 33 | throw Error('private key is too short to contain ID byte'); 34 | } 35 | 36 | return privateKeyFromId(bytes[0], bytes.slice(1)); 37 | } 38 | -------------------------------------------------------------------------------- /packages/crypto/src/multikey/index.ts: -------------------------------------------------------------------------------- 1 | export * from './names'; 2 | export * from './bytes'; 3 | export * from './ids'; 4 | -------------------------------------------------------------------------------- /packages/crypto/src/multikey/names.ts: -------------------------------------------------------------------------------- 1 | import { KindSECP256K1, KindED25519, KindSECP256R1 } from '../keys'; 2 | import { PublicKey } from '../public'; 3 | import { SECP256K1PublicKey } from '../secp256k1/public'; 4 | import { ED25519PublicKey } from '../ed25519/public'; 5 | import { SECP256R1PublicKey } from '../secp256r1'; 6 | 7 | export function kindFromPublicKey(key: PublicKey): string { 8 | switch (key.constructor) { 9 | case SECP256K1PublicKey: 10 | return KindSECP256K1; 11 | case ED25519PublicKey: 12 | return KindED25519; 13 | case SECP256R1PublicKey: 14 | return KindSECP256R1; 15 | default: 16 | throw RangeError('unknown public key type'); 17 | } 18 | } 19 | 20 | export function publicKeyFromKind(kind: string, data: Uint8Array): PublicKey { 21 | switch (kind) { 22 | case KindSECP256K1: 23 | return new SECP256K1PublicKey(data); 24 | case KindED25519: 25 | return new ED25519PublicKey(data); 26 | case KindSECP256R1: 27 | return new SECP256R1PublicKey(data); 28 | default: 29 | throw RangeError('unknown public key type'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/crypto/src/private.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from './public'; 2 | 3 | export interface PrivateKey extends SignerWithPublicKey { 4 | readonly bytes: Uint8Array; 5 | } 6 | 7 | export interface SignerWithPublicKey extends Signer { 8 | readonly publicKey: PublicKey; 9 | } 10 | 11 | export interface Signer { 12 | sign(message: Uint8Array): Promise; 13 | curve: string; 14 | } 15 | -------------------------------------------------------------------------------- /packages/crypto/src/public.ts: -------------------------------------------------------------------------------- 1 | export interface PublicKey { 2 | readonly bytes: Uint8Array; 3 | verify: (message: Uint8Array, sig: Uint8Array) => Promise; 4 | curve: string; 5 | } 6 | 7 | /** 8 | * Compares two {@link PublicKey} objects for equality. 9 | * @param a The first {@link PublicKey} to compare. 10 | * @param b The second {@link PublicKey} to compare. 11 | * @returns True if both {@link PublicKey} are equal, false otherwise. 12 | */ 13 | export function isPublicKeyEqual(a: PublicKey, b: PublicKey): boolean { 14 | if (a.curve !== b.curve) { 15 | return false; 16 | } 17 | 18 | if (a.bytes.length !== a.bytes.length) { 19 | return false; 20 | } 21 | 22 | return a.bytes.every((value, index) => value === b.bytes[index]); 23 | } 24 | 25 | export function isPublicKey(x: any): x is PublicKey { 26 | if (typeof x !== 'object') return false; 27 | if (!(x.bytes instanceof Uint8Array)) return false; 28 | if (!(x.verify instanceof Function)) return false; 29 | if (typeof x.curve !== 'string') return false; 30 | 31 | return true; 32 | } 33 | -------------------------------------------------------------------------------- /packages/crypto/src/rand.test.const.ts: -------------------------------------------------------------------------------- 1 | export function testRandomFunction(num?: number): Uint8Array { 2 | return new Uint8Array([ 3 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 4 | 30, 31, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 5 | 28, 29, 30, 31, 6 | ]).slice(0, num || 32); 7 | } 8 | -------------------------------------------------------------------------------- /packages/crypto/src/rand.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from '@noble/hashes/utils'; 2 | 3 | export type RandomFunction = (len?: number) => Uint8Array; 4 | export const secureRandom = randomBytes; 5 | -------------------------------------------------------------------------------- /packages/crypto/src/scrypt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './scrypt'; 2 | -------------------------------------------------------------------------------- /packages/crypto/src/scrypt/scrypt.ts: -------------------------------------------------------------------------------- 1 | import { scrypt } from '@noble/hashes/scrypt'; 2 | import { secureRandom } from '../rand'; 3 | 4 | export type ScryptParams = { 5 | N: number; 6 | r: number; 7 | p: number; 8 | keyLen: number; 9 | }; 10 | 11 | export const defaultScryptParams: ScryptParams = { 12 | // N is the N parameter of Scrypt encryption algorithm, using 256MB 13 | // memory and taking approximately 1s CPU time on a modern processor. 14 | N: 1 << 18, 15 | // P is the P parameter of Scrypt encryption algorithm, using 256MB 16 | // memory and taking approximately 1s CPU time on a modern processor. 17 | p: 1, 18 | r: 8, 19 | keyLen: 32, 20 | }; 21 | 22 | interface Result { 23 | params: ScryptParams; 24 | secret: Uint8Array; 25 | salt: Uint8Array; 26 | } 27 | 28 | export function deriveSecretFromScrypt( 29 | passphrase = '', 30 | params: ScryptParams = defaultScryptParams, 31 | salt: Uint8Array = secureRandom(32), 32 | ): Result { 33 | const secret = scrypt(new Uint8Array(Buffer.from(passphrase, 'ascii')), salt, { 34 | N: params.N, 35 | r: params.r, 36 | p: params.p, 37 | dkLen: params.keyLen, 38 | }); 39 | 40 | return { 41 | params, 42 | secret, 43 | salt, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256k1/index.ts: -------------------------------------------------------------------------------- 1 | export { SECP256K1PrivateKey } from './private'; 2 | export { SECP256K1PublicKey } from './public'; 3 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256k1/private.ts: -------------------------------------------------------------------------------- 1 | import secp256k1 from 'secp256k1'; 2 | import { RandomFunction, secureRandom } from '../rand'; 3 | import { KindSECP256K1 } from '../keys'; 4 | import { PrivateKey } from '../private'; 5 | import { SECP256K1PublicKey } from './public'; 6 | export const PrivateKeyLen = 32; 7 | 8 | const { privateKeyVerify, publicKeyCreate, ecdsaSign } = secp256k1; 9 | 10 | export class SECP256K1PrivateKey implements PrivateKey { 11 | bytes: Uint8Array; 12 | readonly publicKey: SECP256K1PublicKey; 13 | readonly curve: string = KindSECP256K1; 14 | 15 | constructor(bytes: Uint8Array) { 16 | this.bytes = bytes; 17 | 18 | if (!privateKeyVerify(this.bytes)) { 19 | throw RangeError('bytes are not a i9valid ECDSA private key'); 20 | } 21 | 22 | this.publicKey = new SECP256K1PublicKey(publicKeyCreate(this.bytes)); 23 | } 24 | static generate(rand: RandomFunction = secureRandom): SECP256K1PrivateKey { 25 | return new this(rand(PrivateKeyLen)); 26 | } 27 | async sign(message: Uint8Array): Promise { 28 | const sigObj = ecdsaSign(message, this.bytes); 29 | 30 | const ret = new Uint8Array(65); 31 | ret.set(sigObj.signature, 0); 32 | ret.set(new Uint8Array([sigObj.recid]), 64); 33 | return ret; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256r1/index.ts: -------------------------------------------------------------------------------- 1 | export { SECP256R1PrivateKey } from './private'; 2 | export { SECP256R1PublicKey } from './public'; 3 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256r1/private.ts: -------------------------------------------------------------------------------- 1 | import { secp256r1 } from '@noble/curves/p256'; 2 | import { RandomFunction, secureRandom } from '../rand'; 3 | import { KindSECP256R1 } from '../keys'; 4 | import { PrivateKey } from '../private'; 5 | import { SECP256R1PublicKey } from './public'; 6 | export const SECP256R1PrivateKeyLen = 32; 7 | 8 | export class SECP256R1PrivateKey implements PrivateKey { 9 | readonly publicKey: SECP256R1PublicKey; 10 | readonly curve: string = KindSECP256R1; 11 | 12 | constructor(public readonly bytes: Uint8Array, private readonly rand: RandomFunction = secureRandom) { 13 | if (this.bytes.length !== SECP256R1PrivateKeyLen || !secp256r1.utils.isValidPrivateKey(this.bytes)) { 14 | throw RangeError('bytes are not a valid secp256r1 private key'); 15 | } 16 | this.publicKey = new SECP256R1PublicKey(secp256r1.getPublicKey(this.bytes)); 17 | this.curve = KindSECP256R1; 18 | } 19 | 20 | static generate(rand: RandomFunction = secureRandom): SECP256R1PrivateKey { 21 | return new SECP256R1PrivateKey(rand(), rand); 22 | } 23 | async sign(message: Uint8Array): Promise { 24 | const sig = secp256r1.sign(message, this.bytes, { lowS: true, extraEntropy: this.rand() }); 25 | return sig.toCompactRawBytes(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/crypto/src/secp256r1/public.ts: -------------------------------------------------------------------------------- 1 | import { secp256r1 } from '@noble/curves/p256'; 2 | import { PublicKey } from '../public'; 3 | import { KindSECP256R1 } from '../keys'; 4 | 5 | export const SECP256R1PublicKeyLength = 33; 6 | 7 | export class SECP256R1PublicKey implements PublicKey { 8 | readonly curve: string = KindSECP256R1; 9 | 10 | constructor(public readonly bytes: Uint8Array) { 11 | if (this.bytes.length !== SECP256R1PublicKeyLength) { 12 | throw RangeError('bytes are not a valid secp256r1 public key'); 13 | } 14 | } 15 | 16 | async verify(message: Uint8Array, sig: Uint8Array): Promise { 17 | return secp256r1.verify(sig, message, this.bytes); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/crypto/src/testing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './private'; 2 | export * from './public'; 3 | -------------------------------------------------------------------------------- /packages/crypto/src/testing/private.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '../public'; 2 | import { PrivateKey } from '../private'; 3 | import { UnknownPublicKey } from './public'; 4 | 5 | export const PrivateKeyLen = 32; 6 | 7 | export class UnknownPrivateKey implements PrivateKey { 8 | bytes: Uint8Array; 9 | publicKey: PublicKey; 10 | curve = 'testcurve'; 11 | 12 | constructor(bytes?: Uint8Array) { 13 | this.bytes = bytes!; 14 | this.publicKey = new UnknownPublicKey(); 15 | } 16 | 17 | async sign(_message: Uint8Array): Promise { 18 | return new Uint8Array(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/crypto/src/testing/public.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '../public'; 2 | 3 | export class UnknownPublicKey implements PublicKey { 4 | readonly bytes: Uint8Array; 5 | readonly curve: string = 'testcurve'; 6 | 7 | constructor() { 8 | this.bytes = new Uint8Array(); 9 | } 10 | 11 | async verify(_message: Uint8Array, _sig: Uint8Array): Promise { 12 | return false; 13 | } 14 | } 15 | 16 | export const AliceUnknownPublicKey = new UnknownPublicKey(); 17 | -------------------------------------------------------------------------------- /packages/crypto/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/encoding/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Encoding 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/encoding 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/encoding 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The encoding package is used by `@mailchain/sdk` to support encoding across different blockchain protocols. 24 | -------------------------------------------------------------------------------- /packages/encoding/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/encoding/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/encoding/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/encoding", 3 | "version": "0.31.0", 4 | "description": "Mailchain encoding tools", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "author": { 8 | "name": "Mailchain", 9 | "email": "support@mailchain.co", 10 | "url": "https://mailchain.com/" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 15 | "directory": "encoding" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 19 | }, 20 | "dependencies": { 21 | "@noble/hashes": "^1.3.0", 22 | "@scure/base": "^1.1.1" 23 | }, 24 | "scripts": { 25 | "build": "run -T tsup", 26 | "test": "run -T jest", 27 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 28 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/encoding/src/ascii.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeAscii, encodeAscii } from './ascii'; 2 | 3 | describe('encode ascii', () => { 4 | it('should decode ascii', () => { 5 | const input = 'Hello World!'; 6 | const actual = decodeAscii(input); 7 | 8 | expect(actual).toEqual( 9 | Uint8Array.from([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21]), 10 | ); 11 | }); 12 | it('should encode ascii', () => { 13 | const input = Uint8Array.from([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21]); 14 | const actual = encodeAscii(input); 15 | expect(actual).toEqual('Hello World!'); 16 | }); 17 | 18 | it('should throw error when decoding non ascii', () => { 19 | const input = 'Здраво Свету!'; 20 | expect(() => decodeAscii(input)).toThrowError('Input is not ASCII'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/encoding/src/ascii.ts: -------------------------------------------------------------------------------- 1 | export function encodeAscii(input: Uint8Array): string { 2 | return Buffer.from(input).toString('ascii'); 3 | } 4 | 5 | export function decodeAscii(input: string): Uint8Array { 6 | if (!isAscii(input)) throw new Error('Input is not ASCII'); 7 | return new Uint8Array(Buffer.from(input, 'ascii')); 8 | } 9 | 10 | export function isAscii(str: string) { 11 | // eslint-disable-next-line no-control-regex 12 | return /^[\x00-\x7F]+$/.test(str); 13 | } 14 | -------------------------------------------------------------------------------- /packages/encoding/src/base32.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase32, encodeBase32 } from './base32'; 2 | describe('Buffer', () => { 3 | it('Encode and decode are the same', () => { 4 | const arr = new Uint8Array(Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 127, 125, 0, 1])); 5 | expect(decodeBase32(encodeBase32(arr))).toEqual(arr); 6 | }); 7 | }); 8 | 9 | describe('DecodeBase32', () => { 10 | const tests = [ 11 | { 12 | input: 'e7yj7gfs6cp2lm7qt6mlf4e7uwz7bh4zqiytgmrte4', 13 | expected: Uint8Array.from([ 14 | 0x27, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 15 | 0xf0, 0x9f, 0x99, 0x82, 0x31, 0x33, 0x32, 0x33, 0x27, 16 | ]), 17 | }, 18 | { 19 | input: 'me', 20 | expected: Uint8Array.from([0x61]), 21 | }, 22 | { 23 | input: 'jbswy3dp', 24 | expected: Uint8Array.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]), 25 | }, 26 | ]; 27 | 28 | test.each(tests)('$name', async (test) => { 29 | expect(decodeBase32(test.input)).toEqual(test.expected); 30 | }); 31 | }); 32 | 33 | describe('EncodeBase32', () => { 34 | const tests = [ 35 | { 36 | input: Uint8Array.from([ 37 | 0x27, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 38 | 0xf0, 0x9f, 0x99, 0x82, 0x31, 0x33, 0x32, 0x33, 0x27, 39 | ]), 40 | expected: 'e7yj7gfs6cp2lm7qt6mlf4e7uwz7bh4zqiytgmrte4', 41 | }, 42 | { 43 | input: Uint8Array.from([0x61]), 44 | expected: 'me', 45 | }, 46 | { 47 | input: Uint8Array.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]), 48 | expected: 'jbswy3dp', 49 | }, 50 | ]; 51 | 52 | test.each(tests)('$name', async (test) => { 53 | expect(encodeBase32(test.input)).toEqual(test.expected); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/encoding/src/base32.ts: -------------------------------------------------------------------------------- 1 | import { utils } from '@scure/base'; 2 | 3 | const BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'; 4 | const coder = utils.chain( 5 | // We define our own chain, the default base32 has padding 6 | utils.radix2(5), 7 | utils.alphabet(BASE32_ALPHABET), 8 | { 9 | decode: (input) => input.split(''), 10 | encode: (input) => input.join(''), 11 | }, 12 | ); 13 | export function decodeBase32(input: string): Uint8Array { 14 | return coder.decode(input); 15 | } 16 | 17 | export function encodeBase32(input: Uint8Array): string { 18 | return coder.encode(input); 19 | } 20 | 21 | export function isBase32(input: string): boolean { 22 | // Check that all characters are valid base32 characters 23 | for (let i = 0; i < input.length; i++) { 24 | const char = input[i]; 25 | if (BASE32_ALPHABET.indexOf(char.toLowerCase()) === -1) { 26 | return false; 27 | } 28 | } 29 | 30 | // Check that the input length is a multiple of 8 bits 31 | if (input.length % 8 !== 0) { 32 | return false; 33 | } 34 | 35 | return true; 36 | } 37 | -------------------------------------------------------------------------------- /packages/encoding/src/base58.ts: -------------------------------------------------------------------------------- 1 | import { base58 } from '@scure/base'; 2 | export function decodeBase58(input: string): Uint8Array { 3 | return base58.decode(input); 4 | } 5 | 6 | export function encodeBase58(input: Uint8Array): string { 7 | return base58.encode(input); 8 | } 9 | 10 | export function isBase58(input: string): boolean { 11 | return /^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/.test(input); 12 | } 13 | -------------------------------------------------------------------------------- /packages/encoding/src/base58Check.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from '@noble/hashes/sha256'; 2 | import { decodeBase58, encodeBase58 } from './base58'; 3 | 4 | export function decodeBase58Check(input: string): Uint8Array { 5 | const validatedPayload = validatePayload(decodeBase58(input)); 6 | if (validatedPayload == null) throw new Error('failed input checksum validation'); 7 | return validatedPayload; 8 | } 9 | 10 | export function encodeBase58Check(input: Uint8Array): string { 11 | const checksum = calcChecksum(input).slice(0, 4); 12 | return encodeBase58(Uint8Array.from([...input, ...checksum])); 13 | } 14 | 15 | // Ref: https://github.com/bitcoinjs/bs58check 16 | function calcChecksum(payload: Uint8Array): Uint8Array { 17 | return sha256(sha256(payload)); 18 | } 19 | 20 | // Ref: https://github.com/bitcoinjs/bs58check 21 | function validatePayload(payloadWithChecksum: Uint8Array): Uint8Array | null { 22 | const payload = payloadWithChecksum.slice(0, -4); 23 | const payloadChecksum = payloadWithChecksum.slice(-4); 24 | const checksum = calcChecksum(payload); 25 | 26 | // Compare the checksum bytes with XOR operator 27 | if ( 28 | (payloadChecksum[0] ^ checksum[0]) | 29 | (payloadChecksum[1] ^ checksum[1]) | 30 | (payloadChecksum[2] ^ checksum[2]) | 31 | (payloadChecksum[3] ^ checksum[3]) 32 | ) { 33 | return null; 34 | } 35 | 36 | return payload; 37 | } 38 | -------------------------------------------------------------------------------- /packages/encoding/src/base64.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase64, encodeBase64 } from './base64'; 2 | 3 | describe('test unicde characters encoded properly', () => { 4 | test('chinese', () => { 5 | expect(Buffer.from(decodeBase64(encodeBase64(Buffer.from('我你')))).toString()).toEqual('我你'); 6 | }); 7 | test('cyrillic', () => { 8 | expect(Buffer.from(decodeBase64(encodeBase64(Buffer.from('привет всем')))).toString()).toEqual('привет всем'); 9 | }); 10 | test('emoji', () => { 11 | expect(Buffer.from(decodeBase64(encodeBase64(Buffer.from('😀😀😀')))).toString()).toEqual('😀😀😀'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/encoding/src/base64.ts: -------------------------------------------------------------------------------- 1 | export function decodeBase64(input: string): Uint8Array { 2 | const output = Buffer.from(input, 'base64'); 3 | if (input.length > 0 && output.length === 0) { 4 | throw new Error('could not decode input'); 5 | } 6 | 7 | return Uint8Array.from(output); 8 | } 9 | 10 | export function encodeBase64(input: Uint8Array): string { 11 | return Buffer.from(input).toString('base64'); 12 | } 13 | 14 | export function decodeBase64UrlSafe(input: string): Uint8Array { 15 | let encoded = input.replace('-', '+').replace('_', '/'); 16 | while (encoded.length % 4) encoded += '='; 17 | const output = Buffer.from(encoded, 'base64'); 18 | if (input.length > 0 && output.length === 0) { 19 | throw new Error('could not decode input'); 20 | } 21 | 22 | return Uint8Array.from(output); 23 | } 24 | 25 | export function encodeBase64UrlSafe(input: Uint8Array): string { 26 | return Buffer.from(input).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); 27 | } 28 | -------------------------------------------------------------------------------- /packages/encoding/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const EncodingTypes = { 2 | /** 3 | * Plain hex encoding value. 4 | */ 5 | Hex: 'hex/plain', 6 | /** 7 | * 0x prefixed hex encoding 8 | */ 9 | Hex0xPrefix: 'hex/0x-prefix', 10 | /** 11 | * With or without 0x prefix 12 | */ 13 | HexAny: 'hex/any', 14 | Base32: 'base32/plain', 15 | Base58: 'base58/plain', 16 | Base64: 'base64/plain', 17 | Base62Url: 'base64/url', 18 | Base58SubstrateAddress: 'base58/ss58-address', 19 | Utf8: 'text/utf-8', 20 | } as const; 21 | 22 | export type EncodingType = typeof EncodingTypes[keyof typeof EncodingTypes]; 23 | -------------------------------------------------------------------------------- /packages/encoding/src/ensure.ts: -------------------------------------------------------------------------------- 1 | import { EncodingType } from './consts'; 2 | import { decode } from './encoding'; 3 | 4 | export function ensureDecoded(value: Uint8Array | string | Buffer, encoding: EncodingType) { 5 | if (typeof value === 'string') { 6 | return decode(encoding, value); 7 | } 8 | 9 | return value; 10 | } 11 | -------------------------------------------------------------------------------- /packages/encoding/src/hex.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeHexZeroX, encodeHexZeroX } from './hex'; 2 | 3 | describe('EncodeHexZeroX', () => { 4 | const tests = [ 5 | { 6 | name: 'success', 7 | input: new Uint8Array([ 8 | 0x56, 0x2, 0xea, 0x95, 0x54, 0xb, 0xee, 0x46, 0xd0, 0x3b, 0xa3, 0x35, 0xee, 0xd6, 0xf4, 0x9d, 0x11, 9 | 0x7e, 0xab, 0x95, 0xc8, 0xab, 0x8b, 0x71, 0xba, 0xe2, 0xcd, 0xd1, 0xe5, 0x64, 0xa7, 0x61, 10 | ]), 11 | expected: '0x5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761', 12 | shouldThrow: false, 13 | }, 14 | ]; 15 | test.each(tests)('$name', async (test) => { 16 | const target = encodeHexZeroX; 17 | 18 | if (test.shouldThrow) { 19 | expect(() => { 20 | target(test.input); 21 | }).toThrow(); 22 | } else { 23 | expect(target(test.input)).toEqual(test.expected); 24 | } 25 | }); 26 | }); 27 | 28 | describe('DecodeHexZeroX', () => { 29 | const tests = [ 30 | { 31 | name: 'success', 32 | input: '0x5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761', 33 | expected: new Uint8Array([ 34 | 0x56, 0x2, 0xea, 0x95, 0x54, 0xb, 0xee, 0x46, 0xd0, 0x3b, 0xa3, 0x35, 0xee, 0xd6, 0xf4, 0x9d, 0x11, 35 | 0x7e, 0xab, 0x95, 0xc8, 0xab, 0x8b, 0x71, 0xba, 0xe2, 0xcd, 0xd1, 0xe5, 0x64, 0xa7, 0x61, 36 | ]), 37 | shouldThrow: false, 38 | }, 39 | { 40 | name: 'err-missing-prefix', 41 | input: '5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761', 42 | expected: null, 43 | shouldThrow: true, 44 | }, 45 | ]; 46 | test.each(tests)('$name', async (test) => { 47 | const target = decodeHexZeroX; 48 | 49 | if (test.shouldThrow) { 50 | expect(() => { 51 | target(test.input); 52 | }).toThrow(); 53 | } else { 54 | expect(target(test.input)).toEqual(test.expected); 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/encoding/src/hex.ts: -------------------------------------------------------------------------------- 1 | // EncodeHexZeroX encodes src into "0x"+hex.Encode. As a convenience, it returns the encoding type used, 2 | // but this value is always TypeHex0XPrefix. 3 | // EncodeHexZeroX uses hexadecimal encoding prefixed with "0x". 4 | export function encodeHexZeroX(input: Uint8Array): string { 5 | return '0x' + encodeHex(input); 6 | } 7 | 8 | // DecodeHexZeroX returns the bytes represented by the hexadecimal string src. 9 | export function decodeHexZeroX(input: string): Uint8Array { 10 | if (input === '') { 11 | throw new RangeError('empty hex string'); 12 | } 13 | 14 | if (!input.startsWith('0x')) { 15 | throw new RangeError("must start with '0x'"); 16 | } 17 | 18 | return decodeHex(input.slice(2)); 19 | } 20 | 21 | // EncodeHex returns the hexadecimal encoding of src. 22 | export function encodeHex(input: Uint8Array): string { 23 | return Buffer.from(input).toString('hex'); 24 | } 25 | 26 | // DecodeHex returns the bytes represented by the hexadecimal string s. 27 | export function decodeHex(input: string): Uint8Array { 28 | const output = new Uint8Array(Buffer.from(input, 'hex')); 29 | if (output.length === 0 || input.length === 0) { 30 | throw RangeError('invalid hex encoding'); 31 | } 32 | return output; 33 | } 34 | 35 | export function isHex(input: string): boolean { 36 | return new RegExp('^[a-fA-F0-9]+$').test(input); 37 | } 38 | 39 | export function isHexZeroX(input: string): boolean { 40 | return new RegExp('^0x[a-fA-F0-9]+$').test(input); 41 | } 42 | 43 | export function isAnyHex(input: string): boolean { 44 | if (input.startsWith('0x')) { 45 | input = input.substring(2); 46 | } 47 | return isHex(input); 48 | } 49 | 50 | export function decodeHexAny(input: string): Uint8Array { 51 | if (input.startsWith('0x')) { 52 | input = input.substring(2); 53 | } 54 | return decodeHex(input); 55 | } 56 | -------------------------------------------------------------------------------- /packages/encoding/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts'; 2 | export * from './ensure'; 3 | export * from './encoding'; 4 | export * from './hex'; 5 | export * from './base32'; 6 | export * from './base58'; 7 | export * from './base58Check'; 8 | export * from './base64'; 9 | export * from './utf8'; 10 | export * from './ascii'; 11 | -------------------------------------------------------------------------------- /packages/encoding/src/utf8.test.ts: -------------------------------------------------------------------------------- 1 | import { decodeUtf8, encodeUtf8 } from './utf8'; 2 | describe('Buffer', () => { 3 | it('Encode and decode are the same', () => { 4 | const arr = new Uint8Array(Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 127, 125, 0, 1])); 5 | expect(decodeUtf8(encodeUtf8(arr))).toEqual(arr); 6 | }); 7 | it('Encode and decode should not be the same because of utf8 sequnce is not valid', () => { 8 | const arr = new Uint8Array(Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 140, 125, 0, 1])); 9 | expect(decodeUtf8(encodeUtf8(arr))).not.toEqual(arr); 10 | }); 11 | }); 12 | 13 | describe('Decode', () => { 14 | const tests = [ 15 | { 16 | input: "'😲🥳😲🥳🙂1323'", 17 | expected: Uint8Array.from([ 18 | 0x27, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 19 | 0xf0, 0x9f, 0x99, 0x82, 0x31, 0x33, 0x32, 0x33, 0x27, 20 | ]), 21 | }, 22 | ]; 23 | 24 | test.each(tests)('$name', async (test) => { 25 | expect(decodeUtf8(test.input)).toEqual(test.expected); 26 | }); 27 | }); 28 | 29 | describe('Encode', () => { 30 | const tests = [ 31 | { 32 | input: Uint8Array.from([ 33 | 0x27, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 0xf0, 0x9f, 0x98, 0xb2, 0xf0, 0x9f, 0xa5, 0xb3, 34 | 0xf0, 0x9f, 0x99, 0x82, 0x31, 0x33, 0x32, 0x33, 0x27, 35 | ]), 36 | expected: "'😲🥳😲🥳🙂1323'", 37 | }, 38 | ]; 39 | 40 | test.each(tests)('$name', async (test) => { 41 | expect(encodeUtf8(test.input)).toEqual(test.expected); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/encoding/src/utf8.ts: -------------------------------------------------------------------------------- 1 | export function encodeUtf8(input: Uint8Array): string { 2 | return Buffer.from(input).toString('utf-8'); 3 | } 4 | 5 | export function decodeUtf8(input: string): Uint8Array { 6 | return new Uint8Array(Buffer.from(input, 'utf-8')); 7 | } 8 | -------------------------------------------------------------------------------- /packages/encoding/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/internal/.gitignore: -------------------------------------------------------------------------------- 1 | src/protobuf 2 | -------------------------------------------------------------------------------- /packages/internal/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Internal Library 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/internal 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/internal 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The internal package is used by `@mailchain/sdk` to support it's operations. It's available to developers to use however the interface is not yet stable so expect changes. Stable interfaces will be exposed to their own packages. 24 | -------------------------------------------------------------------------------- /packages/internal/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/internal/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/internal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/internal", 3 | "version": "0.31.0", 4 | "description": "Mailchain internal library", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "internal" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@mailchain/addressing": "0.31.0", 21 | "@mailchain/api": "0.31.0", 22 | "@mailchain/crypto": "0.31.0", 23 | "@mailchain/encoding": "0.31.0", 24 | "@mailchain/keyring": "0.31.0", 25 | "@mailchain/message-composer": "0.31.0", 26 | "@mailchain/signatures": "0.31.0", 27 | "@noble/hashes": "^1.3.0", 28 | "axios": "1.6.0", 29 | "canonicalize": "^1.0.8", 30 | "did-jwt-vc": "3.1.0", 31 | "did-resolver": "^4.1.0", 32 | "email-addresses": "5.0.0", 33 | "emailjs-mime-parser": "^2.0.7", 34 | "lodash": "^4.17.21", 35 | "long": "^5.2.1", 36 | "protobufjs": "^7.2.5", 37 | "striptags": "^3.2.0" 38 | }, 39 | "devDependencies": { 40 | "jest": "^29.7.0", 41 | "jest-mock-extended": "^3.0.5", 42 | "typescript": "^5.1.3" 43 | }, 44 | "scripts": { 45 | "build": "run -T tsup", 46 | "test": "run -T jest", 47 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 48 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/internal/rollup.const.js: -------------------------------------------------------------------------------- 1 | export const explicitExports = [ 2 | '@mailchain/internal/transport/mailer/mailerProof', 3 | '@mailchain/internal/sending/verifiablePresentationRequest', 4 | '@mailchain/internal/verifiableCredentials', 5 | ]; 6 | -------------------------------------------------------------------------------- /packages/internal/src/configuration.ts: -------------------------------------------------------------------------------- 1 | export type Configuration = { 2 | apiPath: string; 3 | mailchainAddressDomain: string; 4 | nearRpcUrl: string; 5 | }; 6 | 7 | export const defaultConfiguration: Configuration = { 8 | apiPath: 'https://api.mailchain.com', 9 | mailchainAddressDomain: 'mailchain.com', 10 | nearRpcUrl: 'https://rpc.near.org', 11 | }; 12 | -------------------------------------------------------------------------------- /packages/internal/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unexpected'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/errors/unexpected.ts: -------------------------------------------------------------------------------- 1 | export class UnexpectedMailchainError extends Error { 2 | readonly type = 'unexpected_error'; 3 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#unexpected_error'; 4 | constructor(message: string, errorOptions?: { cause?: Error }) { 5 | super(message, errorOptions); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/internal/src/errors/validation.ts: -------------------------------------------------------------------------------- 1 | export class ValidationError extends Error { 2 | constructor(message: string, errorOptions?: { cause?: Error }) { 3 | super(message, errorOptions); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/internal/src/formatters/__snapshots__/parse.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`parse should handle legacy message content and subject 1`] = ` 4 | { 5 | "blindCarbonCopyRecipients": [ 6 | { 7 | "address": "rec5@mailchain.local", 8 | "name": "rec5", 9 | }, 10 | { 11 | "address": "rec6@mailchain.local", 12 | "name": "rec6", 13 | }, 14 | ], 15 | "carbonCopyRecipients": [ 16 | { 17 | "address": "rec3@mailchain.local", 18 | "name": "rec3", 19 | }, 20 | { 21 | "address": "rec4@mailchain.local", 22 | "name": "rec4", 23 | }, 24 | ], 25 | "date": 2022-06-06T00:00:00.000Z, 26 | "from": { 27 | "address": "1337@mailchain.com", 28 | "name": "1337", 29 | }, 30 | "id": "123@mailchain.local", 31 | "message": "

Lorem ipsum dolor sit amet

Лорем ипсум долор сит амет

Λορεμ ιπσθμ δολορ σιτ αμετ

側経意責家方家閉討店暖育田庁載社転線宇

🐺🏢🔹🔯🍵 🕒

", 32 | "plainTextMessage": undefined, 33 | "recipients": [ 34 | { 35 | "address": "rec1@mailchain.local", 36 | "name": "rec1", 37 | }, 38 | { 39 | "address": "rec2@mailchain.local", 40 | "name": "rec2", 41 | }, 42 | ], 43 | "replyTo": undefined, 44 | "subject": "Lorem ipsum dolor sit ametЛорем ипсум долор сит аметΛορεμ ιπσθμ δολορ σιτ αμετ側経意責家方家閉討店暖育田庁載社転線宇🐺🏢🔹🔯🍵 🕒", 45 | } 46 | `; 47 | -------------------------------------------------------------------------------- /packages/internal/src/formatters/consts.ts: -------------------------------------------------------------------------------- 1 | export const X_IDENTITY_KEYS = 'X-IdentityKeys' as const; 2 | -------------------------------------------------------------------------------- /packages/internal/src/formatters/generate.test.ts: -------------------------------------------------------------------------------- 1 | import { dummyMailData, dummyMailDataResolvedAddresses } from '../test.const'; 2 | import { createMimeMessage } from './generate'; 3 | 4 | describe('createMimeMessage', () => { 5 | it('should generate mail message', async () => { 6 | const messages = await createMimeMessage(dummyMailData, dummyMailDataResolvedAddresses); 7 | 8 | expect(removeRandomBoundaries(messages.original)).toMatchSnapshot('ORIGINAL'); 9 | expect(removeRandomBoundaries(messages.visibleRecipients)).toMatchSnapshot('VISIBLE'); 10 | expect(removeRandomBoundaries(messages.blindRecipients[0].content)).toMatchSnapshot( 11 | `BLIND ${dummyMailData.blindCarbonCopyRecipients[0].address}`, 12 | ); 13 | expect(removeRandomBoundaries(messages.blindRecipients[1].content)).toMatchSnapshot( 14 | `BLIND ${dummyMailData.blindCarbonCopyRecipients[1].address}`, 15 | ); 16 | }); 17 | }); 18 | 19 | function removeRandomBoundaries(msg: string): string { 20 | msg = msg.replaceAll(/boundary.*/g, `boundary="boundary"`); 21 | msg = msg.replaceAll(/--.*/g, `--boundary`); 22 | 23 | return msg; 24 | } 25 | -------------------------------------------------------------------------------- /packages/internal/src/formatters/parse.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { parseMimeText } from './parse'; 3 | 4 | describe('parse', () => { 5 | it('should handle legacy message content and subject', async () => { 6 | const legacyContent = readFileSync(__dirname + '/__tests__/legacy-content-and-subject.eml').toString(); 7 | 8 | const result = await parseMimeText(Buffer.from(legacyContent)); 9 | 10 | expect(result.mailData).toMatchSnapshot(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/internal/src/formatters/simpleMimeHeaderParser.ts: -------------------------------------------------------------------------------- 1 | export function simpleMimeHeaderParser(message: string): Map { 2 | const result = new Map(); 3 | const headersBlankLine = message.indexOf('\r\n\r\n'); 4 | const headersPart = message.substring(0, headersBlankLine); 5 | const lines = headersPart.split('\r\n'); 6 | 7 | let rawHeader = ''; 8 | for (const line of lines) { 9 | if (/^\s/.test(line)) { 10 | // starts with white-space, continuing from previous line 11 | rawHeader += ' ' + line.trimStart(); 12 | } else { 13 | if (rawHeader.length > 0) { 14 | const [key, value] = processRawHeader(rawHeader); 15 | result.set(key, value); 16 | } 17 | rawHeader = line; 18 | } 19 | } 20 | // Last line is not handled by the for loop 21 | if (rawHeader.length > 0) { 22 | const [key, value] = processRawHeader(rawHeader); 23 | result.set(key, value); 24 | } 25 | 26 | return result; 27 | } 28 | 29 | function processRawHeader(rawHeader: string): [string, string] { 30 | const colonIndex = rawHeader.indexOf(':'); 31 | if (colonIndex > 0) { 32 | const key = rawHeader.substring(0, colonIndex).trim(); 33 | const value = rawHeader.substring(colonIndex + 1, rawHeader.length).trim(); 34 | return [key, value]; 35 | } 36 | throw new Error(`invalid header formatting [${rawHeader}]`); 37 | } 38 | -------------------------------------------------------------------------------- /packages/internal/src/identityKeys/index.ts: -------------------------------------------------------------------------------- 1 | export * from './identityKeys'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Configuration } from './configuration'; 2 | export type { MailchainResult } from './mailchainResult'; 3 | export { partitionMailchainResults } from './mailchainResult'; 4 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/index.ts: -------------------------------------------------------------------------------- 1 | export { MailchainMailboxOperations } from './mailboxOperations'; 2 | export type { MailboxOperations } from './mailboxOperations'; 3 | export { MailboxStorage } from './mailboxStorage'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/messageCrypto.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceKeyRing, bobKeyRing } from '@mailchain/keyring/test.const'; 2 | import { KindNaClSecretKey, secureRandom } from '@mailchain/crypto'; 3 | import { BobED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 4 | import { EncodingTypes } from '@mailchain/encoding'; 5 | import { Payload } from '../transport'; 6 | import { createMailchainMessageCrypto } from './messageCrypto'; 7 | 8 | const testPayload: Payload = { 9 | Content: Buffer.from(secureRandom(20 * 1024)), // 20KB, 10 | Headers: { 11 | Origin: BobED25519PublicKey, 12 | ContentSignature: secureRandom(), 13 | Created: new Date('11.27.2023'), 14 | ContentLength: 10 * 1024, 15 | ContentType: 'message/x.mailchain', 16 | ContentEncoding: EncodingTypes.Base64, 17 | ContentEncryption: KindNaClSecretKey, 18 | }, 19 | }; 20 | 21 | describe('createMailchainMessageCrypto', () => { 22 | beforeEach(() => { 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | it('should roundtrip', async () => { 27 | const messageCrypto = createMailchainMessageCrypto(aliceKeyRing); 28 | 29 | const encryptedPayload = await messageCrypto.encrypt(testPayload); 30 | const decryptedPayload = await messageCrypto.decrypt(encryptedPayload); 31 | 32 | expect(decryptedPayload).toEqual(testPayload); 33 | }); 34 | 35 | it('should fail roundtrip with different keys', async () => { 36 | const messageCrypto = createMailchainMessageCrypto(aliceKeyRing); 37 | 38 | const encryptedPayload = await messageCrypto.encrypt(testPayload); 39 | 40 | const messageCrypto2 = createMailchainMessageCrypto(bobKeyRing); 41 | await expect(messageCrypto2.decrypt(encryptedPayload)).rejects.toThrowError(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/messageCrypto.ts: -------------------------------------------------------------------------------- 1 | import { EncryptedContent, ED25519ExtendedPrivateKey } from '@mailchain/crypto'; 2 | import { KeyRing } from '@mailchain/keyring'; 3 | import { deserialize, serialize, CHUNK_LENGTH_1MB, decryptPayload, encryptPayload } from '../transport/serialization'; 4 | import { Payload } from '../transport'; 5 | import { SerializablePayloadHeadersImpl } from '../transport/payload/headersSerialize'; 6 | 7 | /** 8 | * Used for encryption and decryption of the full message content. 9 | */ 10 | export type MessageCrypto = { 11 | encrypt: (payload: Payload) => Promise; 12 | decrypt: (content: EncryptedContent) => Promise; 13 | }; 14 | 15 | export function createMailchainMessageCrypto(keyRing: KeyRing): MessageCrypto { 16 | const inboxKey = ED25519ExtendedPrivateKey.fromPrivateKey(keyRing.rootInboxKey()); 17 | const headersSerializer = new SerializablePayloadHeadersImpl(); 18 | 19 | const encrypt: MessageCrypto['encrypt'] = async (payload) => { 20 | const headers = headersSerializer.serialize(payload.Headers); 21 | const encryptedPayload = await encryptPayload(headers, payload.Content, inboxKey, CHUNK_LENGTH_1MB); 22 | const serializedContent = serialize(encryptedPayload); 23 | return new Uint8Array(serializedContent); 24 | }; 25 | 26 | const decrypt: MessageCrypto['decrypt'] = async (serializedContent) => { 27 | const encryptedPayload = deserialize(Buffer.from(serializedContent)); 28 | const { headers, content } = await decryptPayload(encryptedPayload, inboxKey); 29 | 30 | return { 31 | Content: content, 32 | Headers: headersSerializer.deserialize(headers), 33 | }; 34 | }; 35 | 36 | return { encrypt, decrypt }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/messageId.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyToBytes, PublicKey } from '@mailchain/crypto'; 2 | import { decodeUtf8, encodeHex } from '@mailchain/encoding'; 3 | import { KeyRing } from '@mailchain/keyring'; 4 | import { sha3_256 } from '@noble/hashes/sha3'; 5 | import { MailData } from '../transport'; 6 | 7 | export type MessageIdCreator = ( 8 | params: 9 | | { mailData: MailData; type: 'sent' } 10 | | { mailData: MailData; type: 'received'; mailbox: PublicKey; owner: string }, 11 | ) => Promise; 12 | 13 | export function createMailchainMessageIdCreator(keyRing: KeyRing): MessageIdCreator { 14 | return async (params) => { 15 | const typeBytes = decodeUtf8(params.type); 16 | const mailIdBytes = decodeUtf8(params.mailData.id); 17 | const mailboxBytes = params.type === 'received' ? publicKeyToBytes(params.mailbox) : new Uint8Array(); 18 | const ownerBytes = params.type === 'received' ? decodeUtf8(params.owner) : new Uint8Array(); 19 | 20 | return encodeHex( 21 | sha3_256( 22 | await keyRing 23 | .rootInboxKey() 24 | .sign(Uint8Array.from([...typeBytes, ...mailIdBytes, ...mailboxBytes, ...ownerBytes])), 25 | ), 26 | ); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/payloadStorage/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payloadStorage'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/payloadStorage/payloadStorage.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from '../../transport'; 2 | 3 | export interface PayloadStorage { 4 | /** 5 | * Check if the payload can be stored in the context of the storage. 6 | * This check does not guarantee that the payload will be stored. 7 | */ 8 | canStorePayload(payload: Payload): Promise; 9 | 10 | /** 11 | * @param payload The payload to store. 12 | * @returns The resource identifier under which the payload is stored. 13 | */ 14 | storePayload(payload: Payload): Promise; 15 | 16 | /** 17 | * Check if the resource identifier is valid in the context of the storage. 18 | * This check does not guarantee that the payload is retrievable nor that it exists. 19 | * 20 | * @param resourceId The resource identifier under which the payload is stored. 21 | * @returns true if the resource identifier is valid. 22 | */ 23 | canGetPayload(messageId: string, resourceId: string): Promise; 24 | 25 | /** 26 | * @param resourceId The resource identifier under which the payload is stored. 27 | * @returns The payload. 28 | * @throws Error if the payload is not found or cannot be retrieved. 29 | */ 30 | getPayload(messageId: string, resourceId: string): Promise; 31 | } 32 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/types.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from '@mailchain/crypto'; 2 | import type { PayloadHeaders } from '../transport/payload/headers'; 3 | 4 | export type MessagesOverview = { 5 | total: number; 6 | unread: number; 7 | folders: Map; 8 | }; 9 | 10 | export type FolderMessagesOverview = { 11 | total: number; 12 | unread: number; 13 | mailboxes: Map; 14 | }; 15 | 16 | export type MailboxMessagesOverview = { 17 | total: number; 18 | unread: number; 19 | }; 20 | 21 | export type MessageKind = 'mail' | 'vc-request'; 22 | 23 | export type MessagePreview = { 24 | kind: MessageKind; 25 | mailbox: PublicKey; 26 | messageId: string; 27 | messageBodyResourceId: string; 28 | from: string; 29 | owner: string; 30 | subject: string; 31 | snippet: string; 32 | isRead: boolean; 33 | systemLabels: SystemMessageLabel[]; 34 | hasAttachment: boolean; 35 | timestamp: Date; 36 | to: string[]; 37 | cc: string[]; 38 | bcc: string[]; 39 | }; 40 | 41 | export type Message = { 42 | replyTo?: string; 43 | body: string; 44 | payloadHeaders: H; 45 | }; 46 | 47 | /** Copy from services/inbox/internal/datastore/labels.go */ 48 | export const SYSTEM_MESSAGE_LABELS = [ 49 | 'archive', 50 | 'draft', 51 | 'inbox', 52 | 'sent', 53 | 'spam', 54 | 'starred', 55 | 'trash', 56 | 'unread', 57 | 'outbox', 58 | ] as const; 59 | export type SystemMessageLabel = (typeof SYSTEM_MESSAGE_LABELS)[number]; 60 | export type UserMessageLabel = string; 61 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/userMailboxHasher.test.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase64 } from '@mailchain/encoding'; 2 | import { aliceKeyRing, bobKeyRing } from '@mailchain/keyring/test.const'; 3 | import { AliceAccountMailbox, AliceWalletMailbox } from '../user/test.const'; 4 | import { createMailchainUserMailboxHasher } from './userMailboxHasher'; 5 | 6 | describe('userMailboxHasher', () => { 7 | it('should create different hash for different mailboxes', async () => { 8 | const hasher = createMailchainUserMailboxHasher(aliceKeyRing); 9 | 10 | const accountMailboxHash = await hasher(AliceAccountMailbox); 11 | const walletMailboxHash = await hasher(AliceWalletMailbox); 12 | 13 | expect(accountMailboxHash).not.toEqual(walletMailboxHash); 14 | expect(encodeBase64(accountMailboxHash)).toEqual('tMqSrUrtyZFQvNMNiC0N+5lA2YO251BGO8rOn14SZcU='); 15 | expect(encodeBase64(walletMailboxHash)).toEqual('V4fWUJ9BMklgfZ5/eIho0ByCp4KoxS+2965MYN2JASY='); 16 | }); 17 | 18 | it('should create different hash for same mailbox but different keyring', async () => { 19 | const aliceHasher = createMailchainUserMailboxHasher(aliceKeyRing); 20 | const bobHasher = createMailchainUserMailboxHasher(bobKeyRing); 21 | 22 | const hashByAlice = await aliceHasher(AliceWalletMailbox); 23 | const hashByBob = await bobHasher(AliceWalletMailbox); 24 | 25 | expect(hashByAlice).not.toEqual(hashByBob); 26 | expect(encodeBase64(hashByAlice)).toEqual('V4fWUJ9BMklgfZ5/eIho0ByCp4KoxS+2965MYN2JASY='); 27 | expect(encodeBase64(hashByBob)).toEqual('Hf0Yilygl9dqW93Ls9VlxiuQJoKwRDzIL0PYMFWK4wM='); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/internal/src/mailbox/userMailboxHasher.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyToBytes } from '@mailchain/crypto'; 2 | import { KeyRing } from '@mailchain/keyring'; 3 | import { sha3_256 } from '@noble/hashes/sha3'; 4 | import { UserMailbox } from '../user/types'; 5 | 6 | export type UserMailboxHasher = (userMailbox: UserMailbox) => Promise; 7 | 8 | export function createMailchainUserMailboxHasher(keyRing: KeyRing): UserMailboxHasher { 9 | return async (userMailbox) => 10 | sha3_256(await keyRing.accountIdentityKey().sign(publicKeyToBytes(userMailbox.identityKey))); 11 | } 12 | -------------------------------------------------------------------------------- /packages/internal/src/mailboxRuleEngine/actions.ts: -------------------------------------------------------------------------------- 1 | import { SystemMessageLabel } from '../mailbox/types'; 2 | import { MailboxRuleAction } from './rule'; 3 | 4 | /** 5 | * Action that should add a system label to a message. 6 | */ 7 | export type ActionAddSystemLabel = MailboxRuleAction & { 8 | type: 'AddSystemLabel'; 9 | }; 10 | 11 | /** 12 | * Type guard for `ActionAddSystemLabel` 13 | */ 14 | export function isActionAddSystemLabel(action: MailboxRuleAction): action is ActionAddSystemLabel { 15 | return action.type === 'AddSystemLabel'; 16 | } 17 | 18 | /** 19 | * Create a `ActionAddSystemLabel` action. 20 | */ 21 | export function actionAddSystemLabel(label: ActionAddSystemLabel['value']): ActionAddSystemLabel { 22 | return { 23 | type: 'AddSystemLabel', 24 | value: label, 25 | }; 26 | } 27 | 28 | /** 29 | * Action that should remove a system label from a message. 30 | */ 31 | export type ActionRemoveSystemLabel = MailboxRuleAction & { 32 | type: 'RemoveSystemLabel'; 33 | }; 34 | 35 | /** 36 | * Type guard for `ActionRemoveSystemLabel` 37 | */ 38 | export function isActionRemoveSystemLabel(action: MailboxRuleAction): action is ActionRemoveSystemLabel { 39 | return action.type === 'RemoveSystemLabel'; 40 | } 41 | 42 | /** 43 | * Create a `ActionRemoveSystemLabel` action. 44 | */ 45 | export function actionRemoveSystemLabel(label: ActionAddSystemLabel['value']): ActionRemoveSystemLabel { 46 | return { 47 | type: 'RemoveSystemLabel', 48 | value: label, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/internal/src/mailboxRuleEngine/index.ts: -------------------------------------------------------------------------------- 1 | export { MailchainUserBlocklistRule } from './mailchainUserBlocklist'; 2 | export { MailchainRuleRepository } from './mailchainRuleRepository'; 3 | export { MailboxRuleEngine } from './mailboxRuleEngine'; 4 | export type { MailboxRule, MailboxRuleAction, MailboxRuleCondition } from './rule'; 5 | export type { RuleApplyParams, RulesSource } from './mailboxRuleEngine'; 6 | -------------------------------------------------------------------------------- /packages/internal/src/mailboxRuleEngine/rule.ts: -------------------------------------------------------------------------------- 1 | export type MailboxRuleCondition = { 2 | type: string; 3 | value: V; 4 | }; 5 | 6 | export type MailboxRuleAction = { 7 | type: string; 8 | value: V; 9 | }; 10 | 11 | export type MailboxRule = { 12 | id: string; 13 | name: string; 14 | isEnabled: () => Promise; 15 | condition: () => Promise>; 16 | actions: () => Promise[]>; 17 | stopFurtherProcessing?: boolean; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/internal/src/mailchainResult.ts: -------------------------------------------------------------------------------- 1 | export type SuccessMailchainResult = { 2 | data: D; 3 | error?: undefined; 4 | }; 5 | export type ErrorMailchainResult = { 6 | error: E; 7 | data?: undefined; 8 | }; 9 | 10 | export type MailchainResult = SuccessMailchainResult | ErrorMailchainResult; 11 | 12 | export type ResultsWithParams = { 13 | result: MailchainResult; 14 | params: P; 15 | }; 16 | 17 | export function partitionMailchainResults( 18 | calls: ResultsWithParams[], 19 | ): { 20 | successes: Array<{ params: P; data: T }>; 21 | failures: Array<{ params: P; error: E }>; 22 | } { 23 | const successes: Array<{ params: P; data: T }> = []; 24 | const failures: Array<{ params: P; error: E }> = []; 25 | 26 | for (const call of calls) { 27 | const { result, params } = call; 28 | if (isErrorMailchainResult(result)) failures.push({ params, error: result.error }); 29 | if (isSuccessMailchainResult(result)) successes.push({ params, data: result.data }); 30 | } 31 | 32 | return { successes, failures }; 33 | } 34 | 35 | function isErrorMailchainResult(r: MailchainResult): r is ErrorMailchainResult { 36 | return r.error != null; 37 | } 38 | 39 | function isSuccessMailchainResult(r: MailchainResult): r is SuccessMailchainResult { 40 | return r.data != null; 41 | } 42 | -------------------------------------------------------------------------------- /packages/internal/src/messageSync/index.ts: -------------------------------------------------------------------------------- 1 | export * from './messageSync'; 2 | export * from './previousMessageSync'; 3 | -------------------------------------------------------------------------------- /packages/internal/src/messagingKeys/contractResolvers/errors.ts: -------------------------------------------------------------------------------- 1 | export class MessagingKeyNotFoundInContractError extends Error { 2 | constructor() { 3 | super(`Messaging key not found in contract.`); 4 | } 5 | } 6 | 7 | export class InvalidContractResponseError extends Error { 8 | constructor(public readonly problem: string) { 9 | super(`Messaging key contract returned invalid response. ${problem}}`); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/internal/src/messagingKeys/contractResolvers/resolver.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolType } from '@mailchain/addressing'; 2 | import { ContractCall } from '@mailchain/api'; 3 | import { PublicKey } from '@mailchain/crypto'; 4 | import { MailchainResult } from '../../'; 5 | import { Proof } from '../proof'; 6 | import { InvalidContractResponseError, MessagingKeyNotFoundInContractError } from './errors'; 7 | 8 | export type ContractMessagingKey = { 9 | messagingKey: PublicKey; 10 | protocol: ProtocolType; 11 | proof: Proof; 12 | }; 13 | export type ContractMessagingKeyError = MessagingKeyNotFoundInContractError | InvalidContractResponseError; 14 | export type ContractCallResolveResult = MailchainResult< 15 | ContractMessagingKey, 16 | MessagingKeyNotFoundInContractError | InvalidContractResponseError 17 | >; 18 | 19 | export interface ContractCallMessagingKeyResolver { 20 | resolve(contract: ContractCall): Promise; 21 | } 22 | 23 | export interface ContractCallLatestNonce { 24 | latestNonce(contract: ContractCall): Promise; 25 | } 26 | -------------------------------------------------------------------------------- /packages/internal/src/messagingKeys/index.ts: -------------------------------------------------------------------------------- 1 | export * from './addressNonce'; 2 | export * from './errors'; 3 | export * from './messagingKeys'; 4 | export * from './messagingKeys'; 5 | export * from './privateMessagingKeys'; 6 | export * from './proof'; 7 | -------------------------------------------------------------------------------- /packages/internal/src/metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metadata'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/migration.test.ts: -------------------------------------------------------------------------------- 1 | import { combineMigrations, nopMigration, MigrationRule } from './migration'; 2 | 3 | describe('migration', () => { 4 | it('should return false for should apply when none of the migration rules require it', async () => { 5 | const shouldApply = await combineMigrations(nopMigration(), nopMigration(), nopMigration()).shouldApply(0); 6 | 7 | expect(shouldApply).toBe(false); 8 | }); 9 | 10 | it('should return true for shouldApply when even single migration rule requires it', async () => { 11 | const shouldApply = await combineMigrations( 12 | nopMigration(), 13 | { ...nopMigration(), shouldApply: () => Promise.resolve(true) }, 14 | nopMigration(), 15 | ).shouldApply(0); 16 | 17 | expect(shouldApply).toBe(true); 18 | }); 19 | 20 | it('should apply migration only of the rules that require it', async () => { 21 | const migration = combineMigrations( 22 | nopMigration(), 23 | incrementNumberMigration(false), 24 | incrementNumberMigration(true), 25 | incrementNumberMigration(true), 26 | incrementNumberMigration(false), 27 | ); 28 | 29 | const shouldApply = await migration.shouldApply(0); 30 | const migrated = await migration.apply(0); 31 | 32 | expect(shouldApply).toEqual(true); 33 | expect(migrated).toEqual(2); 34 | }); 35 | }); 36 | 37 | function incrementNumberMigration(shouldApply: boolean): MigrationRule { 38 | return { 39 | shouldApply: () => Promise.resolve(shouldApply), 40 | apply: (data) => Promise.resolve(data + 1), 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /packages/internal/src/migration.ts: -------------------------------------------------------------------------------- 1 | /** Generic rule for migration of data */ 2 | export type MigrationRule = { 3 | /** 4 | * Perform the required checks to check if migration is required for the provided `data`. 5 | * 6 | * Tip: better make this check fast without checking every aspect of the data because it might run multiple times. 7 | * If it ends up that migration is not required, just return the same data {@link MigrationRule.apply}. 8 | */ 9 | shouldApply: (data: T) => Promise; 10 | /** 11 | * Perform migration of the provided `data`, making sure not to mutate the input `data`. 12 | */ 13 | apply: (data: T) => Promise; 14 | }; 15 | 16 | /** No-operation migration. Useful when no migration is required and to provide this as default. */ 17 | export function nopMigration(): MigrationRule { 18 | return { 19 | shouldApply: () => Promise.resolve(false), 20 | apply: (data) => Promise.resolve(data), 21 | }; 22 | } 23 | 24 | /** Combine multiple migration rules into a single, where only required migrations will be applied. */ 25 | export function combineMigrations(...migrations: MigrationRule[]): MigrationRule { 26 | return { 27 | shouldApply: async (data) => { 28 | for (const migration of migrations) { 29 | if (await migration.shouldApply(data)) return true; 30 | } 31 | return false; 32 | }, 33 | apply: async (data) => { 34 | let latestData = data; 35 | for (const migration of migrations) { 36 | if (await migration.shouldApply(latestData)) { 37 | latestData = await migration.apply(latestData); 38 | } 39 | } 40 | return latestData; 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /packages/internal/src/nameservices/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nameservices'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/deliveryRequests/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deliveryRequests'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/index.ts: -------------------------------------------------------------------------------- 1 | export { MailReceiver } from './mail'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/mail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mail'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/mailer/author.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from '../../configuration'; 2 | import { parseMimeText } from '../../formatters/parse'; 3 | import { parseMailerContentFromJSON, SenderVerifier, MailerPayload } from '../../transport'; 4 | 5 | export class MailerAuthorVerifier { 6 | constructor(private readonly senderVerifier: SenderVerifier) {} 7 | 8 | static create(configuration: Configuration) { 9 | return new MailerAuthorVerifier(SenderVerifier.create(configuration)); 10 | } 11 | 12 | /** 13 | * Checks if the author of the mailer is the same as the from address. 14 | * @param payload - The mailer payload 15 | * @returns 16 | */ 17 | async verifyAuthorOwnsFromAddress(payload: MailerPayload, rfcMail: Buffer): Promise { 18 | const mailerContent = parseMailerContentFromJSON(payload.Content.toString()); 19 | const parsedContent = await parseMimeText(rfcMail); 20 | 21 | if (mailerContent.authorMailAddress.address !== parsedContent.mailData.from.address) { 22 | throw new Error('author address does not match from address'); 23 | } 24 | 25 | return await this.senderVerifier.verifySenderOwnsFromAddress( 26 | parsedContent.mailData.from.address, 27 | mailerContent.authorMessagingKey, 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/mailer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mailer'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/receiving/payload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './payload'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/sending/deliveryRequests/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deliveryRequests'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/sending/distributor/index.ts: -------------------------------------------------------------------------------- 1 | export type { SentPayloadDistributionRequests } from './deliveryRequests'; 2 | export * from './distributor'; 3 | -------------------------------------------------------------------------------- /packages/internal/src/sending/errors.ts: -------------------------------------------------------------------------------- 1 | export class PreflightCheckError extends Error { 2 | readonly type = 'preflight_check_error'; 3 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#preflight_check_failed'; 4 | constructor(message: string) { 5 | super(`${message}. Try again after fixing the problem.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/internal/src/sending/index.ts: -------------------------------------------------------------------------------- 1 | export { PreflightCheckError } from './errors'; 2 | export * from './distributor'; 3 | -------------------------------------------------------------------------------- /packages/internal/src/sending/mail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prepare'; 2 | export * from './sender'; 3 | export type { Address, Attachment, SendMailParams } from './sendMailParams'; 4 | -------------------------------------------------------------------------------- /packages/internal/src/sending/mail/payloads.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithPublicKey } from '@mailchain/crypto'; 2 | import { Distribution, MailData, Payload } from '../../transport'; 3 | import { ResolvedAddress } from '../../messagingKeys'; 4 | import { createMimeMessage } from '../../formatters/generate'; 5 | import { createPayload } from '../payload'; 6 | 7 | export async function createMailPayloads( 8 | senderMessagingKey: SignerWithPublicKey, 9 | resolvedAddresses: Map, 10 | mailData: MailData, 11 | payloadPluginHeaders?: Record, 12 | ): Promise<{ 13 | original: Payload; 14 | distributions: Distribution[]; 15 | }> { 16 | const message = await createMimeMessage(mailData, resolvedAddresses); 17 | 18 | const original = await createPayload( 19 | senderMessagingKey, 20 | Buffer.from(message.original), 21 | 'message/x.mailchain', 22 | payloadPluginHeaders, 23 | ); 24 | const visibleRecipientsPayload = await createPayload( 25 | senderMessagingKey, 26 | Buffer.from(message.visibleRecipients), 27 | 'message/x.mailchain', 28 | payloadPluginHeaders, 29 | ); 30 | const blindRecipients = await Promise.all( 31 | message.blindRecipients.map(async ({ recipient, content }) => ({ 32 | recipients: [recipient.address], 33 | payload: await createPayload( 34 | senderMessagingKey, 35 | Buffer.from(content), 36 | 'message/x.mailchain', 37 | payloadPluginHeaders, 38 | ), 39 | })), 40 | ); 41 | 42 | return { 43 | original, 44 | distributions: [ 45 | { 46 | recipients: [ 47 | ...mailData.recipients.map((x) => x.address), 48 | ...mailData.carbonCopyRecipients.map((x) => x.address), 49 | ], 50 | payload: visibleRecipientsPayload, 51 | }, 52 | ...blindRecipients, 53 | ], 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/internal/src/sending/payload/create.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PrivateKey, BobED25519PrivateKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { createPayload } from './create'; 3 | 4 | describe('createPayload', () => { 5 | const content = Buffer.from('content'); 6 | 7 | beforeAll(() => { 8 | jest.useFakeTimers().setSystemTime(new Date('2022-06-06')); 9 | }); 10 | afterAll(() => { 11 | jest.useRealTimers(); 12 | }); 13 | it('should create a payload - alice key', async () => { 14 | expect( 15 | await createPayload( 16 | AliceED25519PrivateKey, 17 | content, 18 | 'application/vnd.mailchain.verified-credential-request', 19 | { plugin: { value: 'value' } }, 20 | ), 21 | ).toMatchSnapshot(); 22 | }); 23 | 24 | it('should create a payload - bob key', async () => { 25 | expect( 26 | await createPayload( 27 | BobED25519PrivateKey, 28 | content, 29 | 'application/vnd.mailchain.verified-credential-request', 30 | { plugin: { value: 'value' } }, 31 | ), 32 | ).toMatchSnapshot(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/internal/src/sending/payload/create.ts: -------------------------------------------------------------------------------- 1 | import { KindNaClSecretKey, SignerWithPublicKey } from '@mailchain/crypto'; 2 | import { EncodingTypes } from '@mailchain/encoding'; 3 | import isArrayBuffer from 'lodash/isArrayBuffer.js'; 4 | import { PayloadHeaders } from '../../transport/payload/headers'; 5 | import { ContentType, Payload } from '../../transport'; 6 | 7 | export async function createPayload( 8 | signerMessagingKey: SignerWithPublicKey, 9 | content: Buffer | Uint8Array, 10 | contentType: C, 11 | pluginHeaders?: Record, 12 | ): Promise>> { 13 | return { 14 | Headers: { 15 | Origin: signerMessagingKey.publicKey, 16 | ContentSignature: await signerMessagingKey.sign(content), 17 | Created: new Date(), 18 | ContentLength: content.length, 19 | ContentType: contentType, 20 | ContentEncoding: EncodingTypes.Base64, 21 | ContentEncryption: KindNaClSecretKey, 22 | PluginHeaders: pluginHeaders, 23 | }, 24 | Content: isArrayBuffer(content) ? Buffer.from(content) : content, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /packages/internal/src/sending/payload/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create'; 2 | export * from './store'; 3 | -------------------------------------------------------------------------------- /packages/internal/src/sending/verifiablePresentationRequest/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sender'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/sending/verifiablePresentationRequest/payload.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from '../../transport/payload'; 2 | import { PayloadHeaders } from '../../transport/payload/headers'; 3 | 4 | export type VerifiablePresentationHeaders = PayloadHeaders<'application/vnd.mailchain.verified-credential-request'>; 5 | 6 | export function isVerifiablePresentationHeaders(headers: PayloadHeaders): headers is VerifiablePresentationHeaders { 7 | return headers.ContentType === 'application/vnd.mailchain.verified-credential-request'; 8 | } 9 | 10 | export type VerifiablePresentationPayload = Payload; 11 | 12 | export function isVerifiablePresentationPayload(payload: Payload): payload is VerifiablePresentationPayload { 13 | return isVerifiablePresentationHeaders(payload.Headers); 14 | } 15 | -------------------------------------------------------------------------------- /packages/internal/src/transport/deliveryRequests/delivery.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | 3 | import { ExtendedPrivateKey, PublicKey, RandomFunction, secureRandom } from '@mailchain/crypto'; 4 | import { protocol } from '../../protobuf/protocol/protocol'; 5 | 6 | import { createEnvelope } from './envelope'; 7 | 8 | export async function createDelivery( 9 | recipientMessagingKey: PublicKey, 10 | messageRootEncryptionKey: ExtendedPrivateKey, 11 | messageURI: string, 12 | rand: RandomFunction = secureRandom, 13 | ): Promise { 14 | const payload = { 15 | envelope: await createEnvelope(recipientMessagingKey, messageRootEncryptionKey, messageURI, rand), 16 | } as protocol.IDelivery; 17 | 18 | const errMsg = protocol.Delivery.verify(payload); 19 | if (errMsg) throw Error(errMsg); 20 | 21 | return protocol.Delivery.create(payload); 22 | } 23 | -------------------------------------------------------------------------------- /packages/internal/src/transport/deliveryRequests/envelope.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { 3 | ExtendedPrivateKey, 4 | PublicKey, 5 | RandomFunction, 6 | secureRandom, 7 | PrivateKeyEncrypter, 8 | ED25519PrivateKey, 9 | privateKeyToBytes, 10 | } from '@mailchain/crypto'; 11 | import { protocol } from '../../protobuf/protocol/protocol'; 12 | import { createECDHKeyBundle } from './keybundle'; 13 | 14 | /** 15 | * 16 | * @param recipientIdentityKey this is a Mailchain identity key. 17 | * @param messageKey root key used to encrypt message and payload 18 | */ 19 | export async function createEnvelope( 20 | recipientMessagingKey: PublicKey, 21 | messageRootEncryptionKey: ExtendedPrivateKey, 22 | messageURI: string, 23 | rand: RandomFunction = secureRandom, 24 | ): Promise { 25 | const keyBundle = await createECDHKeyBundle(recipientMessagingKey, rand); 26 | const encrypter = PrivateKeyEncrypter.fromPrivateKey(ED25519PrivateKey.fromSeed(keyBundle.secret), rand); 27 | const encryptedMessageKey = await encrypter.encrypt( 28 | privateKeyToBytes(messageRootEncryptionKey.privateKey as ED25519PrivateKey), 29 | ); //TODO: look into encoding extended keys 30 | const encryptedMessageURI = await encrypter.encrypt(Buffer.from(messageURI, 'utf8')); 31 | 32 | const payload = { 33 | encryptedMessageKey, 34 | encryptedMessageUri: encryptedMessageURI, 35 | ecdhKeyBundle: keyBundle.keyBundle, 36 | } as protocol.IEnvelope; 37 | 38 | const errMsg = protocol.Envelope.verify(payload); 39 | if (errMsg) throw Error(errMsg); 40 | 41 | return protocol.Envelope.create(payload); 42 | } 43 | -------------------------------------------------------------------------------- /packages/internal/src/transport/deliveryRequests/index.ts: -------------------------------------------------------------------------------- 1 | export { createDelivery } from './delivery'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/transport/deliveryRequests/keybundle.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/naming-convention */ 2 | import { PublicKey, RandomFunction, secureRandom, publicKeyToBytes, ED25519KeyExchange } from '@mailchain/crypto'; 3 | import { protocol } from '../../protobuf/protocol/protocol'; 4 | 5 | export async function createECDHKeyBundle( 6 | recipientMessagingKey: PublicKey, 7 | rand: RandomFunction = secureRandom, 8 | ): Promise<{ 9 | keyBundle: protocol.ECDHKeyBundle; 10 | secret: Uint8Array; 11 | }> { 12 | const keyEx = new ED25519KeyExchange(rand); 13 | const ephemeralKey = await keyEx.EphemeralKey(); 14 | const sharedSecret = await keyEx.SharedSecret(ephemeralKey, recipientMessagingKey); 15 | 16 | const payload = { 17 | publicMessagingKey: publicKeyToBytes(recipientMessagingKey), 18 | publicEphemeralKey: publicKeyToBytes(ephemeralKey.publicKey), 19 | } as protocol.IECDHKeyBundle; 20 | 21 | const errMsg = protocol.ECDHKeyBundle.verify(payload); 22 | if (errMsg) { 23 | throw Error(errMsg); 24 | } 25 | 26 | return { 27 | secret: sharedSecret, 28 | keyBundle: protocol.ECDHKeyBundle.create(payload), 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /packages/internal/src/transport/distribution/distribution.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from '../payload'; 2 | 3 | export type Distribution = { 4 | recipients: string[]; 5 | payload: Payload; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/internal/src/transport/distribution/index.ts: -------------------------------------------------------------------------------- 1 | export * from './distribution'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/transport/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deliveryRequests'; 2 | export * from './distribution'; 3 | export * from './mail'; 4 | export * from './mailer'; 5 | export * from './payload'; 6 | export * from './verifier'; 7 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mail/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mail/types.ts: -------------------------------------------------------------------------------- 1 | export type MailAddress = { 2 | name: string; 3 | address: string; 4 | }; 5 | 6 | export type MailData = { 7 | id: string; 8 | subject: string; 9 | from: MailAddress; 10 | replyTo?: MailAddress; 11 | date: Date; 12 | recipients: MailAddress[]; 13 | carbonCopyRecipients: MailAddress[]; 14 | blindCarbonCopyRecipients: MailAddress[]; 15 | message: string; 16 | plainTextMessage: string; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mailer/__snapshots__/content.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createContentBuffer example payload: original 1`] = `"{"authorMailAddress":"alice@mailchain.com","authorMessagingKey":"e2723caa23a5b511af5ad7b7ef6076e414ab7e75a9dc910ea60e417a2b770a5671","contentUri":"https://example.com","date":1591401600000,"mailerProof":{"params":{"authorContentSignature":"05060708","expires":1654473600000,"mailerMessagingKey":"e22e322f8740c60172111ac8eadcdda2512f90d06d0e503ef189979a159bece1e8"},"signature":"0a0b0c0d","version":"1.0"},"messageId":"message-id-0","to":["bob@mailchain.com"],"version":"1"}"`; 4 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mailer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './content'; 2 | export * from './mailerProof'; 3 | export * from './payload'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mailer/mailerProof.test.ts: -------------------------------------------------------------------------------- 1 | import { BobED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { MailerProof } from '@mailchain/signatures'; 3 | import { createMailerProofBuffer, parseMailerProofFromJSON } from './mailerProof'; 4 | 5 | describe('round trip', () => { 6 | it('example payload', () => { 7 | const original: MailerProof = { 8 | params: { 9 | expires: new Date('2022-06-06'), 10 | mailerMessagingKey: BobED25519PublicKey, 11 | authorContentSignature: new Uint8Array([0x05, 0x06, 0x07, 0x08]), 12 | }, 13 | signature: new Uint8Array([0x0a, 0xb, 0xc, 0xd]), 14 | version: '1.0', 15 | }; 16 | 17 | const bufferedContent = createMailerProofBuffer(original); 18 | 19 | const actual = parseMailerProofFromJSON(bufferedContent); 20 | 21 | expect(actual).toEqual(original); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/internal/src/transport/mailer/types.ts: -------------------------------------------------------------------------------- 1 | import { MailAddress } from '../mail/types'; 2 | 3 | export type MailerData = { 4 | /** 5 | * Message subject line, this will become the `Subject:` header in the email. 6 | */ 7 | subject: string; 8 | /** 9 | * Reply-To address, this will become the `Reply-To:` header in the email. 10 | */ 11 | replyTo?: MailAddress; 12 | /** 13 | * HTML formatted message, this will become the `text/html` part of the email. 14 | */ 15 | html: string; 16 | /** 17 | * Plain text formatted message, this will become the `text/plain` part of the email. 18 | */ 19 | plainTextMessage: string; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/internal/src/transport/payload/headers.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@mailchain/crypto'; 2 | 3 | export type ContentType = 4 | | 'application/json' 5 | | 'message/x.mailchain' 6 | | 'message/x.mailchain-mailer' 7 | | 'application/vnd.mailchain.verified-credential-request'; 8 | 9 | /** 10 | * PayloadHeaders are the headers provide information about the payload. 11 | */ 12 | export type PayloadHeaders = { 13 | /** 14 | * public key of sender to verify the contents 15 | */ 16 | Origin: PublicKey; 17 | /** 18 | * used to verify un-encrypted contents 19 | */ 20 | ContentSignature: Uint8Array; 21 | 22 | Created: Date; 23 | 24 | // Standard 25 | 26 | /** 27 | * The size of the resource, in decimal number of bytes. 28 | * The Content-Length header indicates the size of the message body, in bytes, sent to the recipient. 29 | */ 30 | ContentLength: number; 31 | 32 | /** 33 | * Indicates the media type of the resource. 34 | * The ContentType representation header is used to indicate the original media type of the resource (prior to any content encoding and encryption applied for sending). 35 | * In responses, a ContentType header provides the client with the actual content type of the returned content. 36 | */ 37 | ContentType: CT; 38 | 39 | /** 40 | * Used to specify the compression algorithm. 41 | */ 42 | ContentEncoding: string; 43 | 44 | /** 45 | * Used to specify the encryption algorithm. 46 | */ 47 | ContentEncryption: string; 48 | 49 | /** 50 | * Indicates an alternate location for the contents if not contained in this object. 51 | */ 52 | ContentLocation?: string; 53 | /** 54 | * Custom set of headers used by plugins. 55 | */ 56 | PluginHeaders?: { 57 | [x: string]: unknown; 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /packages/internal/src/transport/payload/index.ts: -------------------------------------------------------------------------------- 1 | export type { Payload } from './payload'; 2 | export { serializeAndEncryptPayload } from './payload'; 3 | export type { ContentType } from './headers'; 4 | export { SerializablePayloadHeadersImpl } from './headersSerialize'; 5 | -------------------------------------------------------------------------------- /packages/internal/src/transport/payload/payload.ts: -------------------------------------------------------------------------------- 1 | import { ED25519ExtendedPrivateKey } from '@mailchain/crypto'; 2 | import { CHUNK_LENGTH_1MB, encryptPayload, serialize } from '../serialization'; 3 | import { SerializablePayloadHeadersImpl } from './headersSerialize'; 4 | import { PayloadHeaders } from './headers'; 5 | 6 | /** 7 | * Payload. 8 | */ 9 | export interface Payload { 10 | /** 11 | * @see defaults to {@link PayloadHeaders} 12 | */ 13 | Headers: H; 14 | /** 15 | * Raw/data/object that are decrypted and parsed base on the the headers. 16 | */ 17 | Content: Buffer; 18 | } 19 | 20 | export async function serializeAndEncryptPayload( 21 | payload: Payload, 22 | payloadRootEncryptionKey: ED25519ExtendedPrivateKey, 23 | ) { 24 | const headers = new SerializablePayloadHeadersImpl().serialize(payload.Headers); 25 | return serialize(await encryptPayload(headers, payload.Content, payloadRootEncryptionKey, CHUNK_LENGTH_1MB)); 26 | } 27 | -------------------------------------------------------------------------------- /packages/internal/src/transport/payload/verifier.test.ts: -------------------------------------------------------------------------------- 1 | import { aliceKeyRing, bobKeyRing } from '@mailchain/keyring/test.const'; 2 | import { ErrorPayloadSignatureInvalid, PayloadOriginVerifier } from './verifier'; 3 | 4 | describe('verifyPayloadOrigin', () => { 5 | it('verified', async () => { 6 | const verifier = new PayloadOriginVerifier(); 7 | const content = Buffer.from([12, 123, 4, 3, 14, 67]); 8 | 9 | verifier.verifyPayloadOrigin({ 10 | Content: content, 11 | Headers: { 12 | ContentEncoding: 'base64/plain', 13 | ContentEncryption: 'nacl-secret-key', 14 | ContentLength: 395, 15 | ContentSignature: await aliceKeyRing.accountMessagingKey().sign(content), 16 | ContentType: 'message/x.mailchain', 17 | Created: new Date('2022-07-13T18:44:48.536Z'), 18 | Origin: aliceKeyRing.accountMessagingKey().publicKey, 19 | }, 20 | }); 21 | }); 22 | 23 | it('invalid signature', async () => { 24 | const verifier = new PayloadOriginVerifier(); 25 | const content = Buffer.from([12, 123, 4, 3, 14, 67]); 26 | 27 | expect.assertions(1); 28 | await verifier 29 | .verifyPayloadOrigin({ 30 | Content: content, 31 | Headers: { 32 | ContentEncoding: 'base64/plain', 33 | ContentEncryption: 'nacl-secret-key', 34 | ContentLength: 395, 35 | ContentSignature: await aliceKeyRing.accountMessagingKey().sign(content), 36 | ContentType: 'message/x.mailchain', 37 | Created: new Date('2022-07-13T18:44:48.536Z'), 38 | Origin: bobKeyRing.accountMessagingKey().publicKey, // incorrect key 39 | }, 40 | }) 41 | .catch((e) => expect(e).toEqual(new ErrorPayloadSignatureInvalid())); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/internal/src/transport/payload/verifier.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from './payload'; 2 | 3 | export class ErrorPayloadSignatureInvalid extends Error { 4 | constructor() { 5 | super('payload signature invalid'); 6 | } 7 | } 8 | 9 | export class PayloadOriginVerifier { 10 | static create() { 11 | return new PayloadOriginVerifier(); 12 | } 13 | 14 | async verifyPayloadOrigin(payload: Payload) { 15 | if (!payload.Headers) { 16 | throw new Error('payload does not contain Headers'); 17 | } 18 | 19 | if (!payload.Headers.Origin) { 20 | throw new Error('payload does not contain Headers.Origin'); 21 | } 22 | 23 | if (!(await payload.Headers.Origin.verify(payload.Content, payload.Headers.ContentSignature))) { 24 | throw new ErrorPayloadSignatureInvalid(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/internal/src/transport/serialization/chunk.test.ts: -------------------------------------------------------------------------------- 1 | import { chunkBuffer } from './chunk'; 2 | 3 | describe('chunkBuffer', () => { 4 | const tests = [ 5 | { 6 | name: 'Uint8Array', 7 | input: new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 8 | size: 3, 9 | expected: [ 10 | Buffer.from(new Uint8Array([0, 1, 2])), 11 | Buffer.from(new Uint8Array([3, 4, 5])), 12 | Buffer.from(new Uint8Array([6, 7, 8])), 13 | Buffer.from(new Uint8Array([9, 10])), 14 | ], 15 | shouldThrow: false, 16 | }, 17 | { 18 | name: 'Buffer', 19 | input: Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]), 20 | size: 3, 21 | expected: [ 22 | Buffer.from(new Uint8Array([0, 1, 2])), 23 | Buffer.from(new Uint8Array([3, 4, 5])), 24 | Buffer.from(new Uint8Array([6, 7, 8])), 25 | Buffer.from(new Uint8Array([9, 10])), 26 | ], 27 | shouldThrow: false, 28 | }, 29 | ]; 30 | tests.forEach((test) => { 31 | it(test.name, () => { 32 | const target = chunkBuffer; 33 | 34 | if (test.shouldThrow) { 35 | expect(() => { 36 | target(test.input, test.size); 37 | }).toThrow(); 38 | } else { 39 | expect(target(test.input, test.size)).toEqual(test.expected); 40 | } 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/internal/src/transport/serialization/chunk.ts: -------------------------------------------------------------------------------- 1 | export const CHUNK_LENGTH_1MB = 1024 * 1024; 2 | 3 | export function chunkBuffer(input: Uint8Array | Buffer, length: number): Buffer[] { 4 | let buffer = input instanceof Buffer ? input : Buffer.from(input); 5 | const result: Buffer[] = []; 6 | 7 | while (buffer.length > length) { 8 | const chunk = buffer.slice(0, length); 9 | buffer = buffer.slice(length); 10 | result.push(chunk); 11 | } 12 | 13 | if (buffer.length) { 14 | result.push(buffer); 15 | } 16 | 17 | return result; 18 | } 19 | -------------------------------------------------------------------------------- /packages/internal/src/transport/serialization/index.ts: -------------------------------------------------------------------------------- 1 | export { CHUNK_LENGTH_1MB } from './chunk'; 2 | export { decryptPayload } from './decrypt'; 3 | export { encryptPayload } from './encrypt'; 4 | export { deserialize, serialize } from './serialization'; 5 | export * from './headers'; 6 | -------------------------------------------------------------------------------- /packages/internal/src/transport/serialization/payload.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encrypted payload. 3 | */ 4 | export interface EncryptedPayload { 5 | EncryptedHeaders: Buffer; 6 | 7 | EncryptedContentChunks: Buffer[]; 8 | } 9 | -------------------------------------------------------------------------------- /packages/internal/src/transport/verifier/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sender'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/transport/verifier/sender.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, isPublicKeyEqual } from '@mailchain/crypto'; 2 | import { Configuration } from '../../configuration'; 3 | import { MessagingKeys } from '../../messagingKeys'; 4 | 5 | export class SenderVerifier { 6 | constructor(private readonly messagingKeys: MessagingKeys) {} 7 | 8 | static create(configuration: Configuration) { 9 | return new SenderVerifier(MessagingKeys.create(configuration)); 10 | } 11 | /** 12 | * 13 | * @param fromAddress address that sent the mail. `From:` header in the mail. 14 | * @param senderMessagingKey public key of the sender. 15 | * @param at Date to resolve the sender messaging key. When no date is provided, the address resolves using the latest block. 16 | * @returns 17 | */ 18 | async verifySenderOwnsFromAddress(fromAddress: string, senderMessagingKey: PublicKey, at?: Date): Promise { 19 | const { data, error } = await this.messagingKeys.resolveIndividual(fromAddress, at); 20 | if (error != null) { 21 | return false; 22 | } 23 | 24 | return isPublicKeyEqual(data.messagingKey, senderMessagingKey); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/internal/src/user/consolidateMailbox.ts: -------------------------------------------------------------------------------- 1 | import { isSameAddress } from '@mailchain/addressing'; 2 | import uniqWith from 'lodash/unionWith.js'; 3 | import { Alias } from './types'; 4 | import { NewUserMailbox } from './userProfile'; 5 | 6 | export function consolidateMailbox(mailbox: NewUserMailbox): NewUserMailbox { 7 | const consolidators = [consolidateMailboxLabel, consolidateMailboxAliases]; 8 | return consolidators.reduce((prev, consolidator) => consolidator(prev), mailbox); 9 | } 10 | 11 | export function consolidateMailboxLabel(mailbox: NewUserMailbox): NewUserMailbox { 12 | return { ...mailbox, label: mailbox.label?.trim() || null }; 13 | } 14 | 15 | /** 16 | * Consolidate the provided mailbox aliases with the following rules: 17 | * - There can be only single alias per `address`. If there are duplicates, only the first one in the entries is kept, the others are ignored. 18 | */ 19 | export function consolidateMailboxAliases(mailbox: NewUserMailbox): NewUserMailbox { 20 | return { 21 | ...mailbox, 22 | aliases: uniqWith(mailbox.aliases, (a, b) => isSameAddress(a.address, b.address)) as [Alias, ...Alias[]], 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/internal/src/user/createAlias.ts: -------------------------------------------------------------------------------- 1 | import { MailchainAddress } from '@mailchain/addressing'; 2 | import { Alias } from './types'; 3 | 4 | export function createMailboxAlias(address: MailchainAddress, params?: Partial>): Alias { 5 | return { 6 | address, 7 | allowSending: params?.allowSending ?? true, 8 | allowReceiving: params?.allowReceiving ?? true, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/internal/src/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './userProfile'; 2 | export * from './types'; 3 | export * from './createAlias'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /packages/internal/src/user/types.ts: -------------------------------------------------------------------------------- 1 | import { MailchainAddress, ProtocolType } from '@mailchain/addressing'; 2 | import { PublicKey } from '@mailchain/crypto'; 3 | 4 | /** 5 | * Represents different type of alias to be set for the {@link UserMailbox}. 6 | */ 7 | export type Alias = { 8 | address: MailchainAddress; 9 | allowSending: boolean; 10 | allowReceiving: boolean; 11 | }; 12 | 13 | /** 14 | * Entity for sending and receiving messages based on the provided identity key and messaging key. 15 | */ 16 | export type UserMailbox = { 17 | id: string; 18 | /** 19 | * - `'account'` - Mailchain account (example: `'alice@mailchain.com'`). 20 | * - `'wallet'` - registered mailbox based blockchain wallet identity key (example: `'0xEBaae0532dF65ee3f1623f324C9620bB84c8af8d@ethereum.mailchain.com'`) 21 | */ 22 | type: 'account' | 'wallet'; 23 | identityKey: PublicKey; 24 | /** The user preferred label to shown for this mailbox. If `null`, no user preferred label is defined, the application is free to compute one at runtime. */ 25 | label: string | null; 26 | /** Will contain at least one {@link Alias}. The first entry in the array is considered as 'default' alias. */ 27 | aliases: [Alias, ...Alias[]]; 28 | /** Ingredients for the creation of private messaging key via the keyring. */ 29 | messagingKeyParams: { 30 | address: Uint8Array; 31 | protocol: ProtocolType; 32 | network: string; 33 | nonce: number; 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /packages/internal/src/user/utils.ts: -------------------------------------------------------------------------------- 1 | import { isPublicKey, PublicKey, publicKeyToBytes } from '@mailchain/crypto'; 2 | import { encodeHex } from '@mailchain/encoding'; 3 | import { UserMailbox } from './types'; 4 | 5 | export function encodeMailbox(mailbox: UserMailbox | PublicKey): string { 6 | let key: PublicKey; 7 | if (isPublicKey(mailbox)) key = mailbox; 8 | else key = mailbox.identityKey; 9 | 10 | return encodeHex(publicKeyToBytes(key)); 11 | } 12 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/__snapshots__/payload.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createVerifiableCredential create - ownerOf 1`] = ` 4 | { 5 | "@context": [ 6 | "https://www.w3.org/2018/credentials/v1", 7 | ], 8 | "credentialSubject": [ 9 | { 10 | "ownerOf": { 11 | "address": "address", 12 | "type": "MailchainMessagingKey", 13 | }, 14 | }, 15 | ], 16 | "issuanceDate": "2000-02-01T00:00:00.000Z", 17 | "issuer": { 18 | "id": "did:mailchain:issuer", 19 | }, 20 | "proof": { 21 | "type": "proof-type", 22 | }, 23 | "termsOfUse": [ 24 | { 25 | "actions": [ 26 | "action", 27 | ], 28 | "assignee": "did:mailchain:assignee", 29 | "assigner": "did:mailchain:assigner", 30 | "effect": "Allow", 31 | "id": "terms-1", 32 | "resources": [ 33 | "*", 34 | ], 35 | "type": "HolderPolicy", 36 | }, 37 | ], 38 | "type": [ 39 | "VerifiableCredential", 40 | "MailchainMessagingKeyCredential", 41 | ], 42 | } 43 | `; 44 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/__snapshots__/presentation.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createPresentationPayload create 1`] = ` 4 | { 5 | "@context": [ 6 | "https://www.w3.org/2018/credentials/v1", 7 | ], 8 | "expirationDate": undefined, 9 | "holder": "did:mailchain:holder", 10 | "issuanceDate": "Sat, 01 Feb 2020 00:00:00 GMT", 11 | "requestId": "request-id", 12 | "type": [ 13 | "VerifiablePresentation", 14 | ], 15 | "verifiableCredential": [ 16 | { 17 | "@context": [ 18 | "https://www.w3.org/2018/credentials/v1", 19 | ], 20 | "credentialSubject": { 21 | "id": "credentialSubjectId", 22 | }, 23 | "issuanceDate": "2000-02-01T00:00:00.000Z", 24 | "issuer": { 25 | "id": "did:mailchain:issuer", 26 | }, 27 | "proof": { 28 | "id": "proof", 29 | }, 30 | "type": [ 31 | "type", 32 | ], 33 | }, 34 | ], 35 | "verifier": "did:mailchain:verifier", 36 | } 37 | `; 38 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/__snapshots__/resolver.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`MailchainDIDMessagingKeyResolver should resolve: result 1`] = ` 4 | { 5 | "didDocument": { 6 | "authentication": [ 7 | { 8 | "controller": "did:mailchain:0xD5ab4CE3605Cd590Db609b6b5C8901fdB2ef7FE6@ethereum.mailchain.com", 9 | "id": "did:mailchain:0xD5ab4CE3605Cd590Db609b6b5C8901fdB2ef7FE6@ethereum.mailchain.com/messaging-key", 10 | "publicKeyHex": "723caa23a5b511af5ad7b7ef6076e414ab7e75a9dc910ea60e417a2b770a5671", 11 | "type": "ED25519SignatureVerification", 12 | }, 13 | ], 14 | "id": "did:mailchain:0xD5ab4CE3605Cd590Db609b6b5C8901fdB2ef7FE6@ethereum.mailchain.com", 15 | "verificationMethod": [], 16 | }, 17 | "didDocumentMetadata": { 18 | "canonicalId": "did:mailchain:0xD5ab4CE3605Cd590Db609b6b5C8901fdB2ef7FE6@ethereum.mailchain.com", 19 | }, 20 | "didResolutionMetadata": { 21 | "contentType": "application/did+ld+json", 22 | }, 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/context.ts: -------------------------------------------------------------------------------- 1 | export const W3C_CREDENTIALS_CONTEXT = 'https://www.w3.org/2018/credentials/v1'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/did.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolType } from '@mailchain/addressing'; 2 | 3 | export type DecentralizedIdentifier = 4 | | MailchainDecentralizedIdentifier 5 | | MailchainMessagingKeyDecentralizedIdentifier; 6 | 7 | export type MailchainDecentralizedIdentifier = `did:mailchain:${S}`; 8 | 9 | export function mailchainAddressDecentralizedIdentifier(address: string): MailchainDecentralizedIdentifier { 10 | return `did:mailchain:${encodeURIComponent(address)}`; 11 | } 12 | 13 | export function isMailchainAddressDecentralizedIdentifier(did: string): did is MailchainDecentralizedIdentifier { 14 | return did.startsWith('did:mailchain:'); 15 | } 16 | 17 | export function mailchainAddressFromDecentralizedIdentifier(did: MailchainDecentralizedIdentifier): string { 18 | return decodeURIComponent(did.replace('did:mailchain:', '')); 19 | } 20 | 21 | export type MailchainMessagingKeyDecentralizedIdentifier = 22 | `did:mailchain:${S}/messaging-key`; 23 | 24 | export function mailchainMessagingKeyDecentralizedIdentifier( 25 | address: string, 26 | ): MailchainMessagingKeyDecentralizedIdentifier { 27 | return `${mailchainAddressDecentralizedIdentifier(address)}/messaging-key`; 28 | } 29 | 30 | export type MailchainBlockchainAddressDecentralizedIdentifier< 31 | P extends ProtocolType = ProtocolType, 32 | S extends string = string, 33 | > = `did:mailchain:${P}:${S}`; 34 | 35 | export function mailchainBlockchainAddressDecentralizedIdentifier

( 36 | protocol: P, 37 | protocolAddress: S, 38 | ): MailchainBlockchainAddressDecentralizedIdentifier { 39 | return `did:mailchain:${protocol}:${protocolAddress}`; 40 | } 41 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request'; 2 | export * from './payload'; 3 | export * from './verifiableMailchainAddressOwner'; 4 | export * from './parseVerifiablePresentationRequest'; 5 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/jwt.ts: -------------------------------------------------------------------------------- 1 | export type VerifiablePresentationJWT = string; 2 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/parseVerifiablePresentationRequest.test.ts: -------------------------------------------------------------------------------- 1 | import canonicalize from 'canonicalize'; 2 | import { parseVerifiablePresentationRequest } from './parseVerifiablePresentationRequest'; 3 | import { VerifiablePresentationRequest } from './request'; 4 | 5 | const originalVcRequest: VerifiablePresentationRequest = { 6 | type: 'MailchainMessagingKeyCredential', 7 | requestId: 'request-id', 8 | version: '1.0', 9 | from: 'example-app@mailchain.test', 10 | to: 'alice@mailhain.test', 11 | actions: ['Authenticate', 'Join Meeting'], 12 | resources: ['meeting/*'], 13 | signedCredentialExpiresAt: new Date('2020-02-03T00:00:00.000Z'), 14 | signedCredentialExpiresAfter: 3600, 15 | requestExpiresAfter: new Date('2020-02-02T00:00:00.000Z'), 16 | nonce: '1234', 17 | approvedCallback: { 18 | url: 'https://example.com/callback', 19 | }, 20 | }; 21 | 22 | describe('parseVerifiablePresentationRequest', () => { 23 | it('should parse a verifiable presentation request', async () => { 24 | const json = canonicalize(originalVcRequest)!; 25 | 26 | const vcRequest = parseVerifiablePresentationRequest(json); 27 | 28 | expect(vcRequest).toEqual(originalVcRequest); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/parseVerifiablePresentationRequest.ts: -------------------------------------------------------------------------------- 1 | import { VerifiablePresentationRequest } from './request'; 2 | 3 | export function parseVerifiablePresentationRequest(json: string): VerifiablePresentationRequest { 4 | const raw = JSON.parse(json); 5 | 6 | return { 7 | ...raw, 8 | signedCredentialExpiresAt: 9 | raw.signedCredentialExpiresAt != null ? new Date(raw.signedCredentialExpiresAt) : undefined, 10 | requestExpiresAfter: raw.requestExpiresAfter != null ? new Date(raw.requestExpiresAfter) : undefined, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/payload.test.ts: -------------------------------------------------------------------------------- 1 | import { createVerifiableCredential } from './payload'; 2 | 3 | describe('createVerifiableCredential', () => { 4 | it('create - ownerOf', () => { 5 | const actual = createVerifiableCredential({ 6 | credentialSubjects: [ 7 | { 8 | ownerOf: { 9 | address: 'address', 10 | type: 'MailchainMessagingKey', 11 | }, 12 | }, 13 | ], 14 | issuanceDate: new Date(2000, 1, 1), 15 | issuerId: 'did:mailchain:issuer', 16 | proof: { 17 | type: 'proof-type', 18 | }, 19 | termsOfUse: [ 20 | { 21 | id: 'terms-1', 22 | actions: ['action'], 23 | assignee: 'did:mailchain:assignee', 24 | assigner: 'did:mailchain:assigner', 25 | effect: 'Allow', 26 | resources: ['*'], 27 | type: 'HolderPolicy', 28 | }, 29 | ], 30 | type: 'MailchainMessagingKeyCredential', 31 | }); 32 | 33 | expect(actual).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/payload.ts: -------------------------------------------------------------------------------- 1 | import { VerifiableCredential } from 'did-jwt-vc'; 2 | import { Proof } from 'did-jwt-vc/lib/types'; 3 | import { DecentralizedIdentifier } from './did'; 4 | import { TermsOfUse } from './termsOfUse'; 5 | import { CredentialSubject } from './subject'; 6 | import { W3C_CREDENTIALS_CONTEXT } from './context'; 7 | 8 | const MailchainMessagingKeyCredential = 'MailchainMessagingKeyCredential'; 9 | export const CredentialPayloadTypes = [MailchainMessagingKeyCredential] as const; 10 | export type CredentialPayloadType = (typeof CredentialPayloadTypes)[number]; 11 | 12 | type CreateVerifiableCredentialParams = { 13 | type: CredentialPayloadType; 14 | credentialSubjects: CredentialSubject[]; 15 | issuanceDate: Date; 16 | issuerId: DecentralizedIdentifier; 17 | termsOfUse: TermsOfUse[]; 18 | proof: Proof; 19 | }; 20 | 21 | /** 22 | * Creates a verifiable credential using a provided proof instead of signature. 23 | */ 24 | export function createVerifiableCredential(params: CreateVerifiableCredentialParams): VerifiableCredential { 25 | const { type, credentialSubjects, issuanceDate, issuerId, proof, termsOfUse } = params; 26 | 27 | return { 28 | '@context': [W3C_CREDENTIALS_CONTEXT], 29 | credentialSubject: credentialSubjects, 30 | type: ['VerifiableCredential', type], 31 | issuanceDate: issuanceDate.toISOString(), 32 | issuer: { 33 | id: issuerId, 34 | }, 35 | termsOfUse, 36 | proof, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/presentation.test.ts: -------------------------------------------------------------------------------- 1 | import { W3C_CREDENTIALS_CONTEXT } from './context'; 2 | import { createPresentationPayload } from './presentation'; 3 | 4 | describe('createPresentationPayload', () => { 5 | it('create', () => { 6 | const actual = createPresentationPayload({ 7 | requestId: 'request-id', 8 | holder: 'did:mailchain:holder', 9 | issuanceDate: new Date(2020, 1, 1), 10 | verifiableCredential: { 11 | '@context': [W3C_CREDENTIALS_CONTEXT], 12 | credentialSubject: { 13 | id: 'credentialSubjectId', 14 | }, 15 | issuanceDate: new Date(2000, 1, 1).toISOString(), 16 | issuer: { id: 'did:mailchain:issuer' }, 17 | proof: { id: 'proof' }, 18 | type: ['type'], 19 | }, 20 | verifier: 'did:mailchain:verifier', 21 | }); 22 | 23 | expect(actual).toMatchSnapshot(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/presentation.ts: -------------------------------------------------------------------------------- 1 | import { PresentationPayload, VerifiableCredential } from 'did-jwt-vc'; 2 | import { DecentralizedIdentifier, MailchainDecentralizedIdentifier } from './did'; 3 | import { W3C_CREDENTIALS_CONTEXT } from './context'; 4 | 5 | type CreatePresentationPayloadParams = { 6 | requestId: string; 7 | verifiableCredential: VerifiableCredential; 8 | verifier: DecentralizedIdentifier; 9 | issuanceDate: Date; 10 | /** 11 | * The date and time that the credential will expire. 12 | */ 13 | expirationDate?: Date; 14 | holder: MailchainDecentralizedIdentifier; 15 | }; 16 | 17 | /** 18 | * Create presentation payload that is then signed to create a verified presentation. 19 | * @param params 20 | * @returns 21 | */ 22 | export function createPresentationPayload(params: CreatePresentationPayloadParams): PresentationPayload { 23 | const { verifiableCredential, verifier, issuanceDate, expirationDate, holder, requestId } = params; 24 | 25 | return { 26 | '@context': [W3C_CREDENTIALS_CONTEXT], 27 | type: ['VerifiablePresentation'], 28 | requestId, 29 | holder, 30 | verifiableCredential: [verifiableCredential], 31 | verifier, 32 | issuanceDate: issuanceDate.toUTCString(), 33 | expirationDate: expirationDate ? expirationDate.toUTCString() : undefined, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/request/index.ts: -------------------------------------------------------------------------------- 1 | export * from './request'; 2 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/resolver.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceSECP256K1PublicAddressStr } from '@mailchain/addressing/protocols/ethereum/test.const'; 2 | import { mock } from 'jest-mock-extended'; 3 | import { AliceED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 4 | import { AliceSECP256K1PublicKey } from '@mailchain/crypto/secp256k1/test.const'; 5 | import { MessagingKeys, Proof } from '../messagingKeys'; 6 | import { MailchainDIDMessagingKeyResolver } from './resolver'; 7 | 8 | describe('MailchainDIDMessagingKeyResolver', () => { 9 | it('should resolve', async () => { 10 | const mockMessagingKeys = mock(); 11 | mockMessagingKeys.resolveIndividual.mockResolvedValue({ 12 | data: { 13 | mailchainAddress: `${AliceSECP256K1PublicAddressStr}@ethereum.mailchain.com`, 14 | messagingKey: AliceED25519PublicKey, 15 | protocol: 'ethereum', 16 | protocolAddress: AliceSECP256K1PublicAddressStr, 17 | type: 'registered', 18 | identityKey: AliceSECP256K1PublicKey, 19 | proof: {} as Proof, 20 | }, 21 | }); 22 | 23 | const target = new MailchainDIDMessagingKeyResolver(mockMessagingKeys); 24 | const result = await target.resolve(`did:mailchain:${AliceSECP256K1PublicAddressStr}@ethereum.mailchain.com`); 25 | expect(result).toMatchSnapshot('result'); 26 | expect(mockMessagingKeys.resolveIndividual).toHaveBeenCalledWith( 27 | `${AliceSECP256K1PublicAddressStr}@ethereum.mailchain.com`, 28 | ); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/subject.ts: -------------------------------------------------------------------------------- 1 | export type CredentialSubject = CredentialSubjectOwnerOfMailchainMessagingKey; 2 | 3 | export type CredentialSubjectOwnerOfMailchainMessagingKey = { 4 | ownerOf: { 5 | type: 'MailchainMessagingKey'; 6 | address: string; 7 | }; 8 | }; 9 | 10 | export function createOwnerOfMessagingKeySubject(address: string): CredentialSubjectOwnerOfMailchainMessagingKey { 11 | return { 12 | ownerOf: { 13 | type: 'MailchainMessagingKey', 14 | address, 15 | }, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/termsOfUse.ts: -------------------------------------------------------------------------------- 1 | import { DecentralizedIdentifier } from './did'; 2 | 3 | export type TermsOfUse = { 4 | type: 'IssuerPolicy' | 'HolderPolicy'; 5 | id?: string; 6 | effect: 'Allow' | 'Deny'; 7 | /** 8 | * assigner formatted as a {@link DecentralizedIdentifier}, is who is defining the terms of use. Often assigner is the issuer of the credential, however it may be another identity. 9 | */ 10 | assigner: DecentralizedIdentifier; 11 | /** 12 | * assigner formatted as a {@link DecentralizedIdentifier}, is who the terms of use is intended for. 13 | */ 14 | assignee: DecentralizedIdentifier; 15 | actions: string[]; 16 | resources: string[]; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/internal/src/verifiableCredentials/verifiableMailchainAddressOwner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './verifier'; 2 | -------------------------------------------------------------------------------- /packages/internal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/keyring/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | **/generated/** 3 | generated/* -------------------------------------------------------------------------------- /packages/keyring/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Keyring 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/keyring 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/keyring 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The keyring package is used by `@mailchain/sdk` to support securely access messaging and account keys. 24 | -------------------------------------------------------------------------------- /packages/keyring/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/keyring/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/keyring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/keyring", 3 | "version": "0.31.0", 4 | "description": "Mailchain keyring", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "keyring" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@mailchain/addressing": "0.31.0", 21 | "@mailchain/crypto": "0.31.0", 22 | "@mailchain/encoding": "0.31.0" 23 | }, 24 | "scripts": { 25 | "build": "run -T tsup", 26 | "test": "run -T jest", 27 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 28 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/keyring/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DERIVATION_PATH_MESSAGING_KEY_ROOT = 'Mailchain.Messaging.Root'; 2 | export const DERIVATION_PATH_IDENTITY_KEY_ROOT = 'Mailchain.Identity.Root'; 3 | export const DERIVATION_PATH_ENCRYPTION_KEY_ROOT = 'Mailchain.Encryption.Root'; 4 | export const DERIVATION_PATH_USER_PROFILE = 'UserProfile'; 5 | export const DERIVATION_PATH_USER_PROFILE_SETTINGS = 'UserProfile.Settings'; 6 | export const DERIVATION_PATH_INBOX_ROOT = 'Inbox'; 7 | export const DERIVATION_PATH_DATE_OFFSET = 'DateOffset'; 8 | -------------------------------------------------------------------------------- /packages/keyring/src/functions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Decrypter, 3 | ED25519KeyExchange, 4 | EncryptedContent, 5 | Encrypter, 6 | PlainContent, 7 | PrivateKey, 8 | PrivateKeyDecrypter, 9 | PublicKey, 10 | SignerWithPublicKey, 11 | ED25519PrivateKey, 12 | } from '@mailchain/crypto'; 13 | 14 | export interface KeyRingDecrypter extends SignerWithPublicKey { 15 | ecdhDecrypt(bundleEphemeralKey: PublicKey, input: EncryptedContent): Promise; 16 | } 17 | 18 | export type InboxKey = Encrypter & Decrypter; 19 | 20 | export function ecdhKeyRingDecrypter(privateKey: PrivateKey): KeyRingDecrypter { 21 | return { 22 | curve: privateKey.curve, 23 | sign: (input) => privateKey.sign(input), 24 | publicKey: privateKey.publicKey, 25 | ecdhDecrypt: async (bundleEphemeralKey: PublicKey, input: Uint8Array) => { 26 | const keyEx = new ED25519KeyExchange(); 27 | const sharedSecret = await keyEx.SharedSecret(privateKey, bundleEphemeralKey); 28 | const decrypter = PrivateKeyDecrypter.fromPrivateKey(ED25519PrivateKey.fromSeed(sharedSecret)); 29 | 30 | return decrypter.decrypt(input); 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/keyring/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './keyring'; 2 | export * from './functions'; 3 | -------------------------------------------------------------------------------- /packages/keyring/src/test.const.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PrivateKey, BobED25519PrivateKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { KeyRing } from './keyring'; 3 | 4 | export const aliceKeyRing = KeyRing.fromPrivateKey(AliceED25519PrivateKey); 5 | export const bobKeyRing = KeyRing.fromPrivateKey(BobED25519PrivateKey); 6 | -------------------------------------------------------------------------------- /packages/keyring/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/message-composer/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Message Composer 2 | 3 | > **Warning** `@mailchain/message-composer` library is in development and may not cover all cases yet. Use with caution. 4 | 5 | RFC-5322 compliant MIME message composer for Node and for the browser. Just add the content you want and let the library take care of formatting it according to the specification. 6 | 7 | ## Usage 8 | 9 | ```ts 10 | const msg = createMessageComposer(messageComposerContext) 11 | // .id('47bdaf40-c3da-49f7-bfc7-66337f35c6c1@mailchain.com') // Optional 12 | .subject('Subject can contain UTF-8 chars including emojis 😉') 13 | // .date(new Date('08/28/2022')) // Optional 14 | .from({ name: 'Bob', address: 'bob@mailchain.com' }) 15 | .recipients('To', { name: 'Alice', address: 'alice@mailchain.com' }) 16 | .recipients('Cc', { name: 'Joe Doe', address: 'joe@mailchain.com' }) 17 | .recipients( 18 | 'Bcc', 19 | { name: 'Jane Doe', address: 'jane@mailchain.com' }, 20 | { name: 'Bob', address: 'bob@mailchain.com' }, 21 | ) 22 | .message('plain', Buffer.from('Plaintext content. Can also contain UTF-8 and emojis 🤐.')) 23 | .message('html', Buffer.from('This is ✨rich-text✨ HTML content.')) 24 | .attachment({ 25 | cid: 'bfcd3a31-646b-4dcd-a1ea-06d37baf7d2e', 26 | contentType: 'image/png', 27 | filename: 'mailchain-logo.png', 28 | content: readFileSync('mailchain-logo.png'), 29 | }); 30 | 31 | const { forSender, forVisibleRecipients, forBlindedRecipients } = await msg.build(); 32 | ``` 33 | 34 | For full usage examples view the [developer docs](https://docs.mailchain.com). 35 | 36 | ## Installing 37 | 38 | Using npm: 39 | 40 | ```bash 41 | $ npm install @mailchain/message-composer 42 | ``` 43 | 44 | Using yarn: 45 | 46 | ```bash 47 | $ yarn add @mailchain/message-composer 48 | ``` 49 | -------------------------------------------------------------------------------- /packages/message-composer/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/message-composer/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/message-composer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/message-composer", 3 | "version": "0.31.0", 4 | "description": "Mailchain MIME message composer. RFC-5322 compliant", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "message-composer" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@mailchain/encoding": "0.31.0", 21 | "date-fns": "^2.29.2" 22 | }, 23 | "scripts": { 24 | "build": "run -T tsup", 25 | "test": "run -T jest", 26 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 27 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 28 | }, 29 | "engines": { 30 | "node": ">=15.x" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "18.11.18" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/message-composer/src/__tests__/.gitignore: -------------------------------------------------------------------------------- 1 | *.eml 2 | -------------------------------------------------------------------------------- /packages/message-composer/src/__tests__/mailchain-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mailchain/mailchain-sdk-js/1787a34d830f8996c7377c6aa63e0953e1269a92/packages/message-composer/src/__tests__/mailchain-logo.png -------------------------------------------------------------------------------- /packages/message-composer/src/consts.ts: -------------------------------------------------------------------------------- 1 | /** Recommended line length limit defined in https://www.rfc-editor.org/rfc/rfc5322#section-2.1.1 */ 2 | export const LINE_LENGTH_FOLD = 78; 3 | 4 | export const CRLF = '\r\n' as const; 5 | 6 | export const HTAB = ' ' as const; 7 | 8 | export const HEADER_LABELS = { 9 | MimeVersion: 'MIME-Version', 10 | MessageId: 'Message-ID', 11 | Subject: 'Subject', 12 | Date: 'Date', 13 | From: 'From', 14 | To: 'To', 15 | Cc: 'Cc', 16 | Bcc: 'Bcc', 17 | ReplyTo: 'Reply-To', 18 | ContentType: 'Content-Type', 19 | ContentTransferEncoding: 'Content-Transfer-Encoding', 20 | ContentDisposition: 'Content-Disposition', 21 | ContentId: 'Content-ID', 22 | } as const; 23 | -------------------------------------------------------------------------------- /packages/message-composer/src/fallback.ts: -------------------------------------------------------------------------------- 1 | import { HEADER_LABELS } from './consts'; 2 | import { createHeader, createMessageIdHeader } from './headerFactories'; 3 | import { MessageComposerContext } from './messageComposerContext'; 4 | import { Address, DateHeader, Header, MessageIdsHeader } from './types'; 5 | 6 | export async function concludeHeaders( 7 | headers: Map>, 8 | ctx: MessageComposerContext, 9 | ): Promise>> { 10 | const finalHeaders = new Map(headers); 11 | 12 | const fromHeader = finalHeaders.get(HEADER_LABELS.From); 13 | if (fromHeader == null) 14 | throw new Error('defining FROM is required, more info https://www.rfc-editor.org/rfc/rfc5322#section-3.6'); 15 | 16 | finalHeaders.set(HEADER_LABELS.MimeVersion, createHeader(HEADER_LABELS.MimeVersion, '1.0')); 17 | if (!finalHeaders.has(HEADER_LABELS.MessageId)) { 18 | finalHeaders.set(HEADER_LABELS.MessageId, await fallbackMessageId(fromHeader.value[0], ctx)); 19 | } 20 | if (!finalHeaders.has(HEADER_LABELS.Date)) { 21 | finalHeaders.set(HEADER_LABELS.Date, await fallbackDate(ctx)); 22 | } 23 | 24 | return finalHeaders; 25 | } 26 | 27 | export async function fallbackMessageId(from: Address, ctx: MessageComposerContext): Promise { 28 | const senderDomain = from.address.split('@')[1]; 29 | const idValue = await ctx.encodeBase64(await ctx.random(32)); 30 | return createMessageIdHeader(HEADER_LABELS.MessageId, [`${idValue}@${senderDomain}`]); 31 | } 32 | 33 | export async function fallbackDate(_ctx: MessageComposerContext): Promise { 34 | return createHeader(HEADER_LABELS.Date, new Date()); 35 | } 36 | -------------------------------------------------------------------------------- /packages/message-composer/src/hasOnlyAscii.ts: -------------------------------------------------------------------------------- 1 | const SP_CHAR_CODE = 32; 2 | const HTAB_CHAR_CODE = 9; 3 | 4 | export function hasOnlyPrintableUsAscii(str: string, allowWSC = true): boolean { 5 | // A field body may be composed of printable US-ASCII characters as well as the space (SP, ASCII value 32) and horizontal tab (HTAB, ASCII value 9) 6 | // https://www.rfc-editor.org/rfc/rfc5322#section-2.2 7 | for (let i = 0; i < str.length; i++) { 8 | const charCode = str.charCodeAt(i); 9 | if ( 10 | (charCode >= 33 && charCode <= 126) || 11 | (allowWSC && (charCode === SP_CHAR_CODE || charCode === HTAB_CHAR_CODE)) 12 | ) { 13 | continue; 14 | } 15 | return false; 16 | } 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /packages/message-composer/src/headerFactories.ts: -------------------------------------------------------------------------------- 1 | import { HEADER_LABELS } from './consts'; 2 | import { Header, HeaderAttribute, MessageIdsHeader } from './types'; 3 | 4 | export function createHeader(label: string, value: T, ...attrs: HeaderAttribute[]): Header { 5 | return { label, value, attrs }; 6 | } 7 | 8 | export function createMessageIdHeader(label: string, ids: string[], ...attrs: HeaderAttribute[]): MessageIdsHeader { 9 | return { 10 | label, 11 | value: { 12 | type: 'message-ids', 13 | ids, 14 | }, 15 | attrs, 16 | }; 17 | } 18 | 19 | export function contentTypeBoundaryHeader(type: 'alternative' | 'mixed', boundary: string): Header { 20 | return createHeader(HEADER_LABELS.ContentType, `multipart/${type}`, ['boundary', boundary]); 21 | } 22 | -------------------------------------------------------------------------------- /packages/message-composer/src/headerOrder.ts: -------------------------------------------------------------------------------- 1 | import { HEADER_LABELS } from './consts'; 2 | import { Header } from './types'; 3 | 4 | /** Definition for the ordering the the headers. It is not something required by specification, but looks prettier. */ 5 | export const HEADER_ORDERS: { [key: string]: number | undefined } = { 6 | [HEADER_LABELS.MimeVersion]: 100, 7 | [HEADER_LABELS.Date]: 90, 8 | [HEADER_LABELS.MessageId]: 80, 9 | [HEADER_LABELS.Subject]: 50, 10 | [HEADER_LABELS.From]: 34, 11 | [HEADER_LABELS.ReplyTo]: 33, 12 | [HEADER_LABELS.To]: 32, 13 | [HEADER_LABELS.Cc]: 31, 14 | [HEADER_LABELS.Bcc]: 30, 15 | [HEADER_LABELS.ContentType]: -100, 16 | [HEADER_LABELS.ContentDisposition]: -101, 17 | [HEADER_LABELS.ContentTransferEncoding]: -102, 18 | [HEADER_LABELS.ContentId]: -103, 19 | } as const; 20 | 21 | /** Compares two headers for their ordering by using the {@link HEADER_ORDERS}. */ 22 | export function byHeaderOrder(a: Header, b: Header): number { 23 | return (HEADER_ORDERS[b.label] ?? 0) - (HEADER_ORDERS[a.label] ?? 0); 24 | } 25 | -------------------------------------------------------------------------------- /packages/message-composer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts'; 2 | export { createMessageComposer } from './messageComposer'; 3 | -------------------------------------------------------------------------------- /packages/message-composer/src/messageComposerContext.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase64, encodeBase64UrlSafe, decodeUtf8 } from '@mailchain/encoding'; 2 | 3 | export type MessageComposerContext = { 4 | random: (len: number) => Promise; 5 | decodeUtf8: (content: string) => Promise; 6 | encodeBase64: (content: Uint8Array, urlSafe?: boolean) => Promise; 7 | }; 8 | 9 | export function defaultMessageComposerContext(): MessageComposerContext { 10 | return { 11 | random: (len) => 12 | globalThis.crypto != null 13 | ? Promise.resolve(globalThis.crypto.getRandomValues(new Uint8Array(len))) 14 | : import('crypto').then(({ webcrypto }) => webcrypto.getRandomValues(new Uint8Array(len))), 15 | decodeUtf8: (content) => Promise.resolve(decodeUtf8(content)), 16 | encodeBase64: (content, urlSafe) => 17 | urlSafe ? Promise.resolve(encodeBase64UrlSafe(content)) : Promise.resolve(encodeBase64(content)), 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/message-composer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/sdk/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/sdk/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/sdk/jest.setup.js: -------------------------------------------------------------------------------- 1 | import { TextEncoder, TextDecoder } from 'util'; 2 | import { Crypto } from '@peculiar/webcrypto'; 3 | 4 | global.TextEncoder = TextEncoder; 5 | global.TextDecoder = TextDecoder; 6 | // Setup from opaque-ts https://github.com/cloudflare/opaque-ts/blob/main/test/jest.setup-file.mjs 7 | if (typeof crypto === 'undefined') { 8 | global.crypto = new Crypto(); 9 | } 10 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/sdk", 3 | "version": "0.31.0", 4 | "description": "Mailchain sdk for sending messages to web3 addresses", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "sdk" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "homepage": "https://docs.mailchain.com/developer", 20 | "dependencies": { 21 | "@mailchain/addressing": "0.31.0", 22 | "@mailchain/crypto": "0.31.0", 23 | "@mailchain/encoding": "0.31.0", 24 | "@mailchain/internal": "0.31.0", 25 | "@mailchain/keyring": "0.31.0" 26 | }, 27 | "engines": { 28 | "node": ">=15.0.0" 29 | }, 30 | "scripts": { 31 | "build": "run -T tsup", 32 | "test": "run -T jest --passWithNoTests", 33 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 34 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 35 | }, 36 | "devDependencies": { 37 | "@peculiar/webcrypto": "^1.4.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/sdk/rollup.const.js: -------------------------------------------------------------------------------- 1 | export const explicitExports = ['@mailchain/sdk/internal/']; 2 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Configuration } from '@mailchain/internal'; 2 | export { Mailchain } from './mailchain'; 3 | export type { SentMail } from './mailchain'; 4 | export type { Address, Attachment, SendMailParams } from '@mailchain/internal/sending/mail'; 5 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/addressNonce.ts: -------------------------------------------------------------------------------- 1 | import { ProtocolType } from '@mailchain/addressing'; 2 | import { defaultConfiguration } from '@mailchain/internal/configuration'; 3 | import { GetMessagingKeyLatestNonceResult, AddressNonce } from '@mailchain/internal/messagingKeys'; 4 | /** 5 | * Get the latest nonce for an address. 6 | * 7 | * @param address the protocol get the latest nonce for. 8 | * @param protocol where to find the address. 9 | * @returns The latest nonce for the given address. 10 | */ 11 | export async function getMessagingKeyLatestNonce( 12 | address: string, 13 | protocol: ProtocolType, 14 | ): Promise { 15 | return AddressNonce.create(defaultConfiguration).getMessagingKeyLatestNonce(address, protocol); 16 | } 17 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export { MailSender } from '@mailchain/internal/sending/mail'; 2 | export { getMessagingKeyLatestNonce } from './addressNonce'; 3 | export { resolveAddress } from './resolvers'; 4 | export { getPrivateMessagingKey } from './privateMessagingKey'; 5 | export type { ResolvedAddress, ResolveAddressError, ResolveAddressResult } from '@mailchain/internal/messagingKeys'; 6 | export * from './keys'; 7 | 8 | // VC Request 9 | export { MailchainAddressOwnershipVerifier } from '@mailchain/internal/verifiableCredentials'; 10 | export { VerifiablePresentationRequestSender } from '@mailchain/internal/sending/verifiablePresentationRequest'; 11 | export type { 12 | VerifiablePresentationRequest, 13 | CredentialPayloadType, 14 | VerifyMailchainAddressOwnershipParams, 15 | VerifyMailchainAddressOwnershipError, 16 | } from '@mailchain/internal/verifiableCredentials'; 17 | export type { 18 | SentVerifiablePresentationRequest, 19 | SendVerifiablePresentationRequestError, 20 | } from '@mailchain/internal/sending/verifiablePresentationRequest'; 21 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/keys.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PrivateKey, AliceED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { AliceSECP256K1PrivateKey, AliceSECP256K1PublicKey } from '@mailchain/crypto/secp256k1/test.const'; 3 | import { AliceSECP256R1PublicKey } from '@mailchain/crypto/secp256r1/test.const'; 4 | import { 5 | privateMessagingKeyFromHex, 6 | privateMessagingKeyToHex, 7 | publicMessagingKeyFromHex, 8 | publicMessagingKeyToHex, 9 | } from './keys'; 10 | 11 | describe('publicMessagingKey()', () => { 12 | const tests = [ 13 | { 14 | name: 'ed25519 alice', 15 | arg: AliceED25519PublicKey, 16 | }, 17 | { 18 | name: 'secp256k1 alice', 19 | arg: AliceSECP256K1PublicKey, 20 | }, 21 | { 22 | name: 'secp256r1 alice', 23 | arg: AliceSECP256R1PublicKey, 24 | }, 25 | ]; 26 | test.each(tests)('$name', async (test) => { 27 | const hex = publicMessagingKeyToHex(test.arg); 28 | expect(publicMessagingKeyFromHex(hex)).toEqual(test.arg); 29 | }); 30 | }); 31 | 32 | describe('privateMessagingKey()', () => { 33 | const tests = [ 34 | { 35 | name: 'ed25519 alice', 36 | arg: AliceED25519PrivateKey, 37 | }, 38 | { 39 | name: 'secp256k1 alice', 40 | arg: AliceSECP256K1PrivateKey, 41 | }, 42 | // { 43 | // name: 'secp256r1 alice', 44 | // arg: AliceSECP256R1PrivateKey, 45 | // }, not tested due to rand function 46 | ]; 47 | test.each(tests)('$name', async (test) => { 48 | const hex = privateMessagingKeyToHex(test.arg); 49 | expect(privateMessagingKeyFromHex(hex)).toEqual(test.arg); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/keys.ts: -------------------------------------------------------------------------------- 1 | import { decodeHexAny, encodeHex } from '@mailchain/encoding'; 2 | import { 3 | PrivateKey, 4 | PublicKey, 5 | privateKeyFromBytes, 6 | privateKeyToBytes, 7 | publicKeyFromBytes, 8 | publicKeyToBytes, 9 | } from '@mailchain/crypto'; 10 | 11 | /** 12 | * Converts a private messaging key from its hexadecimal representation to a {@link PrivateKey} object. 13 | * @param hex - The hexadecimal representation of the private messaging key. 14 | * @returns The {@link PrivateKey} object. 15 | */ 16 | export function privateMessagingKeyFromHex(hex: string): PrivateKey { 17 | return privateKeyFromBytes(decodeHexAny(hex)); 18 | } 19 | 20 | /** 21 | * Converts a private messaging key from a {@link PrivateKey} object to its hexadecimal representation. 22 | * @param key - The {@link PrivateKey} object. 23 | * @returns The hexadecimal representation of the private messaging key. 24 | */ 25 | export function privateMessagingKeyToHex(key: PrivateKey): string { 26 | return encodeHex(privateKeyToBytes(key)); 27 | } 28 | 29 | /** 30 | * Converts a public messaging key from its hexadecimal representation to a {@link PublicKey} object. 31 | * @param hex - The hexadecimal representation of the public messaging key. 32 | * @returns The {@link PublicKey} object. 33 | */ 34 | export function publicMessagingKeyFromHex(hex: string): PublicKey { 35 | return publicKeyFromBytes(decodeHexAny(hex)); 36 | } 37 | 38 | /** 39 | * Converts a public messaging key from a {@link PublicKey} object to its hexadecimal representation. 40 | * @param key - The {@link PublicKey} object. 41 | * @returns The hexadecimal representation of the public messaging key. 42 | */ 43 | export function publicMessagingKeyToHex(key: PublicKey): string { 44 | return encodeHex(publicKeyToBytes(key)); 45 | } 46 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/privateMessagingKey.ts: -------------------------------------------------------------------------------- 1 | import { PrivateKey } from '@mailchain/crypto'; 2 | import { MailchainResult } from '@mailchain/internal'; 3 | import { defaultConfiguration } from '@mailchain/internal/configuration'; 4 | import { GetExportablePrivateMessagingKeyError, PrivateMessagingKeys } from '@mailchain/internal/messagingKeys'; 5 | import { KeyRing } from '@mailchain/keyring'; 6 | 7 | export async function getPrivateMessagingKey( 8 | address: string, 9 | keyRing: KeyRing, 10 | ): Promise> { 11 | return PrivateMessagingKeys.create(defaultConfiguration).getExportablePrivateMessagingKey(address, keyRing); 12 | } 13 | -------------------------------------------------------------------------------- /packages/sdk/src/internal/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { MessagingKeys, ResolveIndividualAddressResult } from '@mailchain/internal/messagingKeys'; 2 | import { Configuration, defaultConfiguration } from '@mailchain/internal/configuration'; 3 | 4 | /** 5 | * Resolve the address and returns proven messaging key for an address. 6 | * @param address Address to resolve. 7 | * 8 | * @returns A {@link ResolvedAddress resolved address}. 9 | * 10 | * @example 11 | * import { resolveAddress } from '@mailchain/sdk'; 12 | * 13 | * const {data: resolvedAddress, error} = await resolveAddress(address); 14 | * if (error != null) // handle error 15 | * else console.log(resolvedAddress); 16 | */ 17 | export async function resolveAddress( 18 | address: string, 19 | config: Configuration = defaultConfiguration, 20 | ): Promise { 21 | return MessagingKeys.create(config).resolveIndividual(address); 22 | } 23 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "allowJs": true 6 | }, 7 | "include": ["src/**/*"], 8 | "typeRoots": ["./src/internal/protobuf/"] 9 | 10 | } 11 | -------------------------------------------------------------------------------- /packages/signatures/.gitignore: -------------------------------------------------------------------------------- 1 | src/api 2 | -------------------------------------------------------------------------------- /packages/signatures/Readme.md: -------------------------------------------------------------------------------- 1 | # Mailchain Signatures 2 | 3 | Mailchain is a messaging protocol that lets users communicate across protocols. Using Mailchain you can send messages to any blockchain address on different protocols. 4 | 5 | For full usage examples view the [developer docs](https://docs.mailchain.com). 6 | 7 | ## Installing 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install @mailchain/signatures 13 | ``` 14 | 15 | Using yarn: 16 | 17 | ```bash 18 | $ yarn add @mailchain/signatures 19 | ``` 20 | 21 | ## Purpose 22 | 23 | The signatures package is used by `@mailchain/sdk` to support creating and verifying proofs. 24 | -------------------------------------------------------------------------------- /packages/signatures/babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/signatures/jest.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { createConfig } = require('../../../../jest.config.cjs'); 3 | 4 | module.exports = { 5 | ...createConfig(path.join(__dirname, '..', '..', '..', '..')), 6 | }; 7 | -------------------------------------------------------------------------------- /packages/signatures/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mailchain/signatures", 3 | "version": "0.31.0", 4 | "description": "Mailchain signatures", 5 | "license": "Apache-2.0", 6 | "author": { 7 | "name": "Mailchain", 8 | "email": "support@mailchain.co", 9 | "url": "https://mailchain.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/mailchain/mailchain-sdk-js.git", 14 | "directory": "signatures" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/mailchain/mailchain-sdk-js/issues" 18 | }, 19 | "dependencies": { 20 | "@ethersproject/hash": "5.7.0", 21 | "@mailchain/addressing": "0.31.0", 22 | "@mailchain/crypto": "0.31.0", 23 | "@mailchain/encoding": "0.31.0", 24 | "@noble/hashes": "^1.3.0", 25 | "canonicalize": "^1.0.8", 26 | "lodash": "^4.17.21" 27 | }, 28 | "devDependencies": { 29 | "@mailchain/addressing": "0.31.0", 30 | "@mailchain/keyring": "0.31.0" 31 | }, 32 | "scripts": { 33 | "build": "run -T tsup", 34 | "test": "run -T jest", 35 | "lint:test": "yarn run -T eslint --ext js,ts,tsx src", 36 | "lint:fix": "yarn run -T eslint --fix --ext js,ts,tsx src" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/signatures/src/__snapshots__/mailer.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`createMailerProofParamsForSigning() version-1.0 1`] = `"{"authorContentSignature":"05060708","expires":1654473600,"mailerMessagingKey":"e22e322f8740c60172111ac8eadcdda2512f90d06d0e503ef189979a159bece1e8"}"`; 4 | -------------------------------------------------------------------------------- /packages/signatures/src/consts.ts: -------------------------------------------------------------------------------- 1 | export const KindEthereumPersonalMessage = 'ethereum_personal_message'; 2 | // KindRawED25519 signs the message with the curve's native sign method with no pre-processing. 3 | export const KindRawED25519 = 'raw_ed25519'; 4 | export const KindTezos = 'tezos_signed_message_micheline'; 5 | export const KindMailchainUsernameIdentityKey = 'mailchain_username_identity_key'; 6 | -------------------------------------------------------------------------------- /packages/signatures/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ProtocolIsEmptyError extends Error { 2 | constructor() { 3 | super('protocol is empty'); 4 | } 5 | } 6 | export class AddressIsEmptyError extends Error { 7 | constructor() { 8 | super('address is empty'); 9 | } 10 | } 11 | 12 | export class MessagingKeyVerificationError extends Error { 13 | readonly type = 'messaging_key_validation_failed'; 14 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#messaging_key_validation_failed'; 15 | constructor() { 16 | super('Messaging key validation failed and is not useable for this address.'); 17 | } 18 | } 19 | 20 | export class ProvidedMessagingKeyIncorrectError extends Error { 21 | readonly type = 'provided_messaging_key_incorrect'; 22 | readonly docs = 'https://docs.mailchain.com/developer/errors/codes#provided_messaging_key_incorrect'; 23 | constructor(type: 'sender' | 'signer' | 'address') { 24 | super(`Provided messaging key does not match. Use the latest messaging key owned by ${type}.`); 25 | } 26 | } 27 | 28 | export class AddressMustBeProtocolAddressError extends Error { 29 | constructor() { 30 | super('address must be a protocol address'); 31 | } 32 | } 33 | 34 | export class PublicKeyNotFoundError extends Error { 35 | constructor() { 36 | super('Mailchain public key not found'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/signatures/src/eth_personal.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, PrivateKey, KindSECP256K1, ErrorUnsupportedKey } from '@mailchain/crypto'; 2 | import { decodeHexZeroX } from '@mailchain/encoding'; 3 | 4 | export async function verifyEthereumPersonalMessage( 5 | key: PublicKey, 6 | message: Uint8Array, 7 | signature: Uint8Array, 8 | ): Promise { 9 | switch (key.curve) { 10 | case KindSECP256K1: 11 | const messageHash = await getMessageHash(message); 12 | return key.verify(messageHash, signature); 13 | default: 14 | throw new ErrorUnsupportedKey(key.curve); 15 | } 16 | } 17 | 18 | export async function signEthereumPersonalMessage(key: PrivateKey, message: Uint8Array): Promise { 19 | switch (key.curve) { 20 | case KindSECP256K1: 21 | const messageHash = await getMessageHash(message); 22 | 23 | return key.sign(messageHash); 24 | default: 25 | throw new ErrorUnsupportedKey(key.curve); 26 | } 27 | } 28 | 29 | export async function getMessageHash(message: Uint8Array): Promise { 30 | const { hashMessage } = await import('@ethersproject/hash'); 31 | return decodeHexZeroX(hashMessage(message)); 32 | } 33 | -------------------------------------------------------------------------------- /packages/signatures/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export * from './eth_personal'; 3 | export * from './mailchain_message_confirmation'; 4 | export * from './mailchain_msgkey'; 5 | export * from './mailchain_password_reset'; 6 | export * from './mailchain_username'; 7 | export * from './raw_ed25519'; 8 | export * from './verify'; 9 | export * from './mailer'; 10 | export * from './consts'; 11 | -------------------------------------------------------------------------------- /packages/signatures/src/keyreg/__snapshots__/message.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CreateProofMessage simple-v1-en_US: simple-v1-en_US 1`] = ` 4 | "Welcome to Mailchain! 5 | 6 | Please sign to start using this address with Mailchain. This will not trigger a blockchain transaction or cost any gas fees. 7 | 8 | What's happening? 9 | A messaging key will be registered with this address and used only for messaging. It will replace any existing registered messaging keys. 10 | 11 | Technical Details: 12 | Address: 0x5602ea95540bee46d03ba335eed6f49d117eab95c8ab8b71bae2cdd1e564a761 13 | Messaging key: 0xe2723caa23a5b511af5ad7b7ef6076e414ab7e75a9dc910ea60e417a2b770a5671 14 | Nonce: 1" 15 | `; 16 | -------------------------------------------------------------------------------- /packages/signatures/src/keyreg/index.ts: -------------------------------------------------------------------------------- 1 | export { createProofMessage } from './message'; 2 | export { getLatestProofParams, getMailchainUsernameParams } from './params'; 3 | export type { ProofParams } from './params'; 4 | -------------------------------------------------------------------------------- /packages/signatures/src/keyreg/message.ts: -------------------------------------------------------------------------------- 1 | import { publicKeyToBytes, PublicKey } from '@mailchain/crypto'; 2 | import { encode } from '@mailchain/encoding'; 3 | import { ProofParams } from './params'; 4 | import { getTemplate } from './templates'; 5 | 6 | export function createProofMessage(params: ProofParams, address: Uint8Array, msgKey: PublicKey, nonce: number): string { 7 | const encodedAddress = encode(params.AddressEncoding, address); 8 | const descriptivePublicKey = publicKeyToBytes(msgKey); 9 | const encodedPublicKey = encode(params.PublicKeyEncoding, descriptivePublicKey); 10 | const templateMessageFunc = getTemplate(params); 11 | 12 | return templateMessageFunc(encodedAddress, encodedPublicKey, nonce); 13 | } 14 | -------------------------------------------------------------------------------- /packages/signatures/src/keyreg/params.ts: -------------------------------------------------------------------------------- 1 | import { EncodingType, EncodingTypes } from '@mailchain/encoding'; 2 | import { ProtocolType, encodingByProtocol } from '@mailchain/addressing'; 3 | 4 | export interface ProofParams { 5 | AddressEncoding: EncodingType; 6 | PublicKeyEncoding: EncodingType; 7 | Locale: string; 8 | Variant: string; 9 | } 10 | 11 | export function getLatestProofParams(protocol: ProtocolType, locale: string): ProofParams { 12 | const addressEncoding = encodingByProtocol(protocol); 13 | return { 14 | AddressEncoding: addressEncoding, 15 | PublicKeyEncoding: EncodingTypes.Hex0xPrefix, 16 | Locale: locale, 17 | Variant: 'simple-v1', 18 | }; 19 | } 20 | 21 | export function getMailchainUsernameParams(): ProofParams { 22 | return { 23 | AddressEncoding: EncodingTypes.Utf8, 24 | PublicKeyEncoding: EncodingTypes.Hex0xPrefix, 25 | Locale: 'en', 26 | Variant: 'mailchain-username', 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/signatures/src/mailchain_message_confirmation.ts: -------------------------------------------------------------------------------- 1 | import { encodeHex } from '@mailchain/encoding'; 2 | import { KeyRingDecrypter } from '@mailchain/keyring'; 3 | 4 | export const mailchainDeliveryConfirmationMessage = (deliveryRequestID: Uint8Array): Uint8Array => { 5 | return Buffer.from(`\x11Mailchain delivery confirmation:\n${encodeHex(deliveryRequestID)}`, 'utf-8'); 6 | }; 7 | 8 | export const signMailchainDeliveryConfirmation = (pk: KeyRingDecrypter, deliveryRequestID: Uint8Array) => { 9 | const msg: Uint8Array = mailchainDeliveryConfirmationMessage(deliveryRequestID); 10 | 11 | return pk.sign(msg); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/signatures/src/mailchain_password_reset.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Signer } from '@mailchain/crypto'; 2 | import { signRawEd25519, verifyRawEd25519 } from './raw_ed25519'; 3 | 4 | export const mailchainPasswordResetMessage = function (username: Uint8Array, expires: Date) { 5 | return Buffer.from( 6 | `\x11Mailchain:\naction: reset-password\nexpires: ${Math.floor( 7 | expires.getTime() / 1000, 8 | )}\nusername: ${username}`, 9 | 'utf-8', 10 | ); 11 | }; 12 | 13 | /** 14 | * Signs a message using the Mailchain username, reset attestation message, and expiration with the identity private key. 15 | */ 16 | export async function signMailchainPasswordReset( 17 | signer: Signer, 18 | username: Uint8Array, 19 | expires: Date, 20 | ): Promise { 21 | return signRawEd25519(signer, mailchainPasswordResetMessage(username, expires)); 22 | } 23 | 24 | /** 25 | * Verifies message linking a username, reset attestation message, and expiration with an identity key is valid. 26 | */ 27 | export async function verifyMailchainPasswordReset( 28 | key: PublicKey, 29 | signature: Uint8Array, 30 | username: Uint8Array, 31 | expires: Date, 32 | ): Promise { 33 | return verifyRawEd25519(key, mailchainPasswordResetMessage(username, expires), signature); 34 | } 35 | -------------------------------------------------------------------------------- /packages/signatures/src/mailchain_username.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, Signer } from '@mailchain/crypto'; 2 | import { signRawEd25519, verifyRawEd25519 } from './raw_ed25519'; 3 | 4 | const mailchainUsernameMessage = function (message: Uint8Array) { 5 | const prefix = Buffer.from(`\x11Mailchain username ownership:\n${message.length}\n`, 'utf-8'); 6 | return Buffer.concat([prefix, message]); 7 | }; 8 | 9 | /** 10 | * Signs a message using the Mailchain username with the identity private key. 11 | * @param key 12 | * @param username 13 | */ 14 | export async function signMailchainUsername(signer: Signer, username: Uint8Array): Promise { 15 | return signRawEd25519(signer, mailchainUsernameMessage(username)); 16 | } 17 | 18 | /** 19 | * Verifies a message linking a username with an identity key is valid. 20 | * @param key 21 | * @param username 22 | * @param signature 23 | * @returns 24 | */ 25 | export async function verifyMailchainUsername( 26 | key: PublicKey, 27 | signature: Uint8Array, 28 | username: Uint8Array, 29 | ): Promise { 30 | return verifyRawEd25519(key, mailchainUsernameMessage(username), signature); 31 | } 32 | -------------------------------------------------------------------------------- /packages/signatures/src/publickey/ethereum.ts: -------------------------------------------------------------------------------- 1 | import { SECP256K1PublicKey } from '@mailchain/crypto'; 2 | import { encodeHex } from '@mailchain/encoding'; 3 | import isEqual from 'lodash/isEqual.js'; 4 | import { ETHEREUM, addressFromPublicKey } from '@mailchain/addressing'; 5 | import { getMessageHash } from '../eth_personal'; 6 | 7 | /** 8 | * Gets the public key from signature and compares it to the expected address to validate it's correct. 9 | * 10 | * The public key that {@link SECP256K1PublicKey.fromSignature} returns will verify the message with the signature. 11 | * However it's not guaranteed that the returned public key will be for the expected address. 12 | * @param message the message being signed 13 | * @param signature the signature of the message made with the private key part of the public key we are trying to extract 14 | */ 15 | export async function ethereumPublicKeyFromSignature( 16 | message: Uint8Array, 17 | signature: Uint8Array, 18 | expectedAddress: Uint8Array, 19 | ): Promise { 20 | const messageHash = await getMessageHash(message); 21 | const publicKey = await SECP256K1PublicKey.fromSignature(messageHash, signature); 22 | 23 | const address = await addressFromPublicKey(publicKey, ETHEREUM); 24 | 25 | if (!isEqual(address, expectedAddress)) { 26 | throw new Error( 27 | `inconsistent public key calculated, expected address "${encodeHex( 28 | expectedAddress, 29 | )}" but actual is "${encodeHex(address)} (both hex encoded)"`, 30 | ); 31 | } 32 | 33 | return publicKey; 34 | } 35 | -------------------------------------------------------------------------------- /packages/signatures/src/publickey/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ethereum'; 2 | -------------------------------------------------------------------------------- /packages/signatures/src/raw_ed25519.test.ts: -------------------------------------------------------------------------------- 1 | import { AliceED25519PrivateKey, AliceED25519PublicKey } from '@mailchain/crypto/ed25519/test.const'; 2 | import { signRawEd25519, verifyRawEd25519 } from './raw_ed25519'; 3 | 4 | describe('Raw EE25519 sign/verify', () => { 5 | it('should sign with ED25519', async () => { 6 | const message = Buffer.from('msg', 'utf-8'); 7 | const expectedSignature = Buffer.from('signed', 'utf-8'); 8 | const mockSign = jest.spyOn(AliceED25519PrivateKey, 'sign'); 9 | mockSign.mockReturnValue(Promise.resolve(expectedSignature)); 10 | 11 | const actualSignature = await signRawEd25519(AliceED25519PrivateKey, message); 12 | 13 | expect(actualSignature).toEqual(expectedSignature); 14 | expect(mockSign.mock.calls[0][0]).toEqual(message); 15 | }); 16 | 17 | it('should verify with ED25519', async () => { 18 | const message = Buffer.from('msg', 'utf-8'); 19 | const signature = Buffer.from('signed', 'utf-8'); 20 | const mockVerify = jest.spyOn(AliceED25519PublicKey, 'verify'); 21 | mockVerify.mockReturnValue(Promise.resolve(true)); 22 | 23 | const valid = await verifyRawEd25519(AliceED25519PublicKey, message, signature); 24 | 25 | expect(valid).toEqual(true); 26 | expect(mockVerify.mock.calls[0]).toEqual([message, signature]); 27 | }); 28 | 29 | afterEach(() => { 30 | jest.resetAllMocks(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/signatures/src/raw_ed25519.ts: -------------------------------------------------------------------------------- 1 | import { KindED25519, Signer, PublicKey, ErrorUnsupportedKey } from '@mailchain/crypto'; 2 | 3 | export async function signRawEd25519(signer: Signer, msg: Uint8Array) { 4 | switch (signer.curve) { 5 | case KindED25519: { 6 | return signer.sign(msg); 7 | } 8 | default: 9 | throw new ErrorUnsupportedKey(signer.curve); 10 | } 11 | } 12 | 13 | export async function verifyRawEd25519(key: PublicKey, msg: Uint8Array, signature: Uint8Array) { 14 | switch (key.curve) { 15 | case KindED25519: 16 | return key.verify(msg, signature); 17 | default: 18 | throw new ErrorUnsupportedKey(key.curve); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/signatures/src/verify.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@mailchain/crypto'; 2 | import { decodeUtf8 } from '@mailchain/encoding'; 3 | import { KindEthereumPersonalMessage, KindRawED25519, KindTezos } from './consts'; 4 | import { verifyEthereumPersonalMessage } from './eth_personal'; 5 | import { verifyRawEd25519 } from './raw_ed25519'; 6 | import { verifyTezosSignedMessage } from './tezos_micheline'; 7 | 8 | export function verify( 9 | signingMethod: string, 10 | verifyingKey: PublicKey, 11 | message: string, 12 | signature: Uint8Array, 13 | ): Promise { 14 | switch (signingMethod) { 15 | case KindEthereumPersonalMessage: 16 | return verifyEthereumPersonalMessage(verifyingKey, decodeUtf8(message), signature); 17 | case KindRawED25519: 18 | return verifyRawEd25519(verifyingKey, decodeUtf8(message), signature); 19 | case KindTezos: 20 | return verifyTezosSignedMessage(verifyingKey, message, signature); 21 | default: 22 | throw new Error(`Signing method ${signingMethod} not supported`); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/signatures/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src/**/*"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------