UNPKG

edge-core-js

Version:

Edge account & wallet management library

176 lines (139 loc) 4.1 kB
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) }