Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/brave-foxes-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ox": patch
---

Added `Ed25519.toX25519PublicKey` and `Ed25519.toX25519PrivateKey` for converting Ed25519 keys to X25519 keys. Useful for performing X25519 Diffie-Hellman key exchange using an Ed25519 signing key pair.
104 changes: 103 additions & 1 deletion src/core/Ed25519.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { ed25519 } from '@noble/curves/ed25519'
import {
ed25519,
edwardsToMontgomeryPriv,
edwardsToMontgomeryPub,
} from '@noble/curves/ed25519'
import * as Bytes from './Bytes.js'
import type * as Errors from './Errors.js'
import * as Hex from './Hex.js'
Expand Down Expand Up @@ -235,3 +239,101 @@ export declare namespace verify {

type ErrorType = Bytes.from.ErrorType | Errors.GlobalErrorType
}

/**
* Converts an Ed25519 public key to an X25519 public key.
*
* This is useful for performing X25519 Diffie-Hellman key exchange
* using an Ed25519 signing key pair.
*
* @example
* ```ts twoslash
* import { Ed25519, X25519 } from 'ox'
*
* const { privateKey, publicKey } = Ed25519.createKeyPair()
*
* const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey })
* ```
*
* @param options - The options.
* @returns The X25519 public key.
*/
export function toX25519PublicKey<as extends 'Hex' | 'Bytes' = 'Hex'>(
options: toX25519PublicKey.Options<as>,
): toX25519PublicKey.ReturnType<as> {
const { as = 'Hex', publicKey } = options
const publicKeyBytes = Bytes.from(publicKey)
const x25519PublicKeyBytes = edwardsToMontgomeryPub(publicKeyBytes)
if (as === 'Hex') return Hex.fromBytes(x25519PublicKeyBytes) as never
return x25519PublicKeyBytes as never
}

export declare namespace toX25519PublicKey {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/**
* Format of the returned public key.
* @default 'Hex'
*/
as?: as | 'Hex' | 'Bytes' | undefined
/** Ed25519 public key to convert. */
publicKey: Hex.Hex | Bytes.Bytes
}

type ReturnType<as extends 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? Bytes.Bytes : never)
| (as extends 'Hex' ? Hex.Hex : never)

type ErrorType =
| Bytes.from.ErrorType
| Hex.fromBytes.ErrorType
| Errors.GlobalErrorType
}

/**
* Converts an Ed25519 private key to an X25519 private key.
*
* This is useful for performing X25519 Diffie-Hellman key exchange
* using an Ed25519 signing key pair.
*
* @example
* ```ts twoslash
* import { Ed25519, X25519 } from 'ox'
*
* const { privateKey, publicKey } = Ed25519.createKeyPair()
*
* const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey })
* ```
*
* @param options - The options.
* @returns The X25519 private key.
*/
export function toX25519PrivateKey<as extends 'Hex' | 'Bytes' = 'Hex'>(
options: toX25519PrivateKey.Options<as>,
): toX25519PrivateKey.ReturnType<as> {
const { as = 'Hex', privateKey } = options
const privateKeyBytes = Bytes.from(privateKey)
const x25519PrivateKeyBytes = edwardsToMontgomeryPriv(privateKeyBytes)
if (as === 'Hex') return Hex.fromBytes(x25519PrivateKeyBytes) as never
return x25519PrivateKeyBytes as never
}

export declare namespace toX25519PrivateKey {
type Options<as extends 'Hex' | 'Bytes' = 'Hex'> = {
/**
* Format of the returned private key.
* @default 'Hex'
*/
as?: as | 'Hex' | 'Bytes' | undefined
/** Ed25519 private key to convert. */
privateKey: Hex.Hex | Bytes.Bytes
}

type ReturnType<as extends 'Hex' | 'Bytes'> =
| (as extends 'Bytes' ? Bytes.Bytes : never)
| (as extends 'Hex' ? Hex.Hex : never)

type ErrorType =
| Bytes.from.ErrorType
| Hex.fromBytes.ErrorType
| Errors.GlobalErrorType
}
105 changes: 104 additions & 1 deletion src/core/_test/Ed25519.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bytes, Ed25519, Hex } from 'ox'
import { Bytes, Ed25519, Hex, X25519 } from 'ox'
import { describe, expect, test } from 'vitest'

describe('createKeyPair', () => {
Expand Down Expand Up @@ -325,6 +325,109 @@ describe('verify', () => {
})
})

describe('toX25519PublicKey', () => {
test('default (Hex)', () => {
const { publicKey } = Ed25519.createKeyPair()
const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey })
expect(typeof x25519PublicKey).toBe('string')
expect(x25519PublicKey).toMatch(/^0x[0-9a-fA-F]{64}$/)
})

test('with as: Bytes', () => {
const { publicKey } = Ed25519.createKeyPair()
const x25519PublicKey = Ed25519.toX25519PublicKey({
publicKey,
as: 'Bytes',
})
expect(x25519PublicKey).toBeInstanceOf(Uint8Array)
expect(x25519PublicKey).toHaveLength(32)
})

test('with Bytes public key', () => {
const { publicKey } = Ed25519.createKeyPair({ as: 'Bytes' })
const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey })
expect(typeof x25519PublicKey).toBe('string')
expect(x25519PublicKey).toMatch(/^0x[0-9a-fA-F]{64}$/)
})

test('deterministic', () => {
const { publicKey } = Ed25519.createKeyPair()
const a = Ed25519.toX25519PublicKey({ publicKey })
const b = Ed25519.toX25519PublicKey({ publicKey })
expect(a).toBe(b)
})

test('different from ed25519 public key', () => {
const { publicKey } = Ed25519.createKeyPair()
const x25519PublicKey = Ed25519.toX25519PublicKey({ publicKey })
expect(x25519PublicKey).not.toBe(publicKey)
})
})

describe('toX25519PrivateKey', () => {
test('default (Hex)', () => {
const { privateKey } = Ed25519.createKeyPair()
const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey })
expect(typeof x25519PrivateKey).toBe('string')
expect(x25519PrivateKey).toMatch(/^0x[0-9a-fA-F]{64}$/)
})

test('with as: Bytes', () => {
const { privateKey } = Ed25519.createKeyPair()
const x25519PrivateKey = Ed25519.toX25519PrivateKey({
privateKey,
as: 'Bytes',
})
expect(x25519PrivateKey).toBeInstanceOf(Uint8Array)
expect(x25519PrivateKey).toHaveLength(32)
})

test('with Bytes private key', () => {
const { privateKey } = Ed25519.createKeyPair({ as: 'Bytes' })
const x25519PrivateKey = Ed25519.toX25519PrivateKey({ privateKey })
expect(typeof x25519PrivateKey).toBe('string')
expect(x25519PrivateKey).toMatch(/^0x[0-9a-fA-F]{64}$/)
})

test('deterministic', () => {
const { privateKey } = Ed25519.createKeyPair()
const a = Ed25519.toX25519PrivateKey({ privateKey })
const b = Ed25519.toX25519PrivateKey({ privateKey })
expect(a).toBe(b)
})
})

describe('toX25519 integration', () => {
test('converted keys produce valid X25519 shared secret', () => {
const alice = Ed25519.createKeyPair()
const bob = Ed25519.createKeyPair()

const aliceX25519Priv = Ed25519.toX25519PrivateKey({
privateKey: alice.privateKey,
})
const bobX25519Pub = Ed25519.toX25519PublicKey({
publicKey: bob.publicKey,
})
const bobX25519Priv = Ed25519.toX25519PrivateKey({
privateKey: bob.privateKey,
})
const aliceX25519Pub = Ed25519.toX25519PublicKey({
publicKey: alice.publicKey,
})

const sharedA = X25519.getSharedSecret({
privateKey: aliceX25519Priv,
publicKey: bobX25519Pub,
})
const sharedB = X25519.getSharedSecret({
privateKey: bobX25519Priv,
publicKey: aliceX25519Pub,
})

expect(sharedA).toBe(sharedB)
})
})

describe('noble export', () => {
test('exports noble curves ed25519', () => {
expect(Ed25519.noble).toBeDefined()
Expand Down
Loading