edge-core-js
Version:
Edge account & wallet management library
176 lines (139 loc) • 4.1 kB
JavaScript
import elliptic from 'elliptic'
import { makeEvents, } from 'yavent'
import {
asLobbyPayload,
wasEdgeLobbyReply,
wasEdgeLobbyRequest
} from '../../types/server-cleaners'
import { decryptText, encrypt } from '../../util/crypto/crypto'
import { hmacSha256, sha256 } from '../../util/crypto/hashes'
import { verifyData } from '../../util/crypto/verify'
import { base58, utf8 } from '../../util/encoding'
import { makePeriodicTask } from '../../util/periodic-task'
import { loginFetch } from './login-fetch'
const EC = elliptic.ec
const secp256k1 = new EC('secp256k1')
/**
* Derives a shared secret from the given secret key and public key.
*/
function deriveSharedKey(keypair, pubkey) {
const secretX = keypair
.derive(secp256k1.keyFromPublic(pubkey).getPublic())
.toArray('be')
// From NIST.SP.800-56Ar2 section 5.8.1:
return hmacSha256(
Uint8Array.from([0, 0, 0, 1, ...secretX]),
utf8.parse('dataKey')
)
}
/**
* Decrypts a lobby reply using the request's secret key.
*/
export function decryptLobbyReply(
keypair,
lobbyReply
) {
const { publicKey } = lobbyReply
const sharedKey = deriveSharedKey(keypair, publicKey)
return JSON.parse(decryptText(lobbyReply.box, sharedKey))
}
/**
* Encrypts a lobby reply JSON replyData, and returns a reply
* suitable for sending to the server.
*/
export function encryptLobbyReply(
io,
publicKey,
replyData
) {
const keypair = secp256k1.genKeyPair({ entropy: io.random(32) })
const sharedKey = deriveSharedKey(keypair, publicKey)
return {
publicKey: Uint8Array.from(keypair.getPublic().encodeCompressed()),
box: encrypt(io, utf8.parse(JSON.stringify(replyData)), sharedKey)
}
}
/**
* Creates a new lobby on the auth server holding the given request.
* @return A lobby watcher object that will check for incoming replies.
*/
export async function makeLobby(
ai,
lobbyRequest,
period = 1000
) {
const { io } = ai.props
const { timeout = 10 * 60 } = lobbyRequest
// Create the keys:
const keypair = secp256k1.genKeyPair({ entropy: io.random(32) })
const publicKey = Uint8Array.from(keypair.getPublic().encodeCompressed())
const lobbyId = base58.stringify(sha256(sha256(publicKey)).slice(0, 10))
const request = {
data: wasEdgeLobbyRequest({ ...lobbyRequest, publicKey, timeout })
}
await loginFetch(ai, 'PUT', `/v2/lobby/${lobbyId}`, request)
// Create the task:
const [on, emit] = makeEvents()
const replies = []
const pollLobby = async () => {
const clean = asLobbyPayload(
await loginFetch(ai, 'GET', '/v2/lobby/' + lobbyId)
)
// Process any new replies that have arrived on the server:
while (replies.length < clean.replies.length) {
const newReply = clean.replies[replies.length]
const fixedReply = decryptLobbyReply(keypair, newReply)
emit('reply', fixedReply)
replies.push(fixedReply)
}
}
const task = makePeriodicTask(pollLobby, period, {
onError(error) {
emit('error', error)
}
})
task.start({ wait: false })
// Create the return object:
return {
close: () => task.stop(),
lobbyId,
on,
replies
}
}
/**
* Fetches a lobby request from the auth server.
* @return A promise of the lobby request JSON.
*/
export async function fetchLobbyRequest(
ai,
lobbyId
) {
const clean = asLobbyPayload(
await loginFetch(ai, 'GET', '/v2/lobby/' + lobbyId)
)
// Verify the public key:
const { publicKey } = clean.request
const checksum = sha256(sha256(publicKey))
const idBytes = base58.parse(lobbyId)
if (!verifyData(idBytes, checksum.subarray(0, idBytes.length))) {
throw new Error('Lobby ECDH integrity error')
}
return clean.request
}
/**
* Encrypts and sends a reply to a lobby request.
*/
export async function sendLobbyReply(
ai,
lobbyId,
lobbyRequest,
replyData
) {
const { io } = ai.props
const { publicKey } = lobbyRequest
const request = {
data: wasEdgeLobbyReply(encryptLobbyReply(io, publicKey, replyData))
}
await loginFetch(ai, 'POST', '/v2/lobby/' + lobbyId, request)
}