Permalink
Cannot retrieve contributors at this time
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
330 lines (285 sloc)
8.65 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* eslint no-bitwise: ["error", {"allow": ["<<"]}] */ | |
| import base32 from 'base32.js'; | |
| import crc from 'crc'; | |
| import isUndefined from 'lodash/isUndefined'; | |
| import isNull from 'lodash/isNull'; | |
| import isString from 'lodash/isString'; | |
| import { verifyChecksum } from './util/checksum'; | |
| const versionBytes = { | |
| ed25519PublicKey: 6 << 3, // G (when encoded in base32) | |
| ed25519SecretSeed: 18 << 3, // S | |
| med25519PublicKey: 12 << 3, // M | |
| preAuthTx: 19 << 3, // T | |
| sha256Hash: 23 << 3, // X | |
| signedPayload: 15 << 3 // P | |
| }; | |
| const strkeyTypes = { | |
| G: 'ed25519PublicKey', | |
| S: 'ed25519SecretSeed', | |
| M: 'med25519PublicKey', | |
| T: 'preAuthTx', | |
| X: 'sha256Hash', | |
| P: 'signedPayload' | |
| }; | |
| /** | |
| * StrKey is a helper class that allows encoding and decoding Stellar keys | |
| * to/from strings, i.e. between their binary (Buffer, xdr.PublicKey, etc.) and | |
| * string (i.e. "GABCD...", etc.) representations. | |
| */ | |
| export class StrKey { | |
| /** | |
| * Encodes `data` to strkey ed25519 public key. | |
| * | |
| * @param {Buffer} data raw data to encode | |
| * @returns {string} "G..." representation of the key | |
| */ | |
| static encodeEd25519PublicKey(data) { | |
| return encodeCheck('ed25519PublicKey', data); | |
| } | |
| /** | |
| * Decodes strkey ed25519 public key to raw data. | |
| * | |
| * If the parameter is a muxed account key ("M..."), this will only encode it | |
| * as a basic Ed25519 key (as if in "G..." format). | |
| * | |
| * @param {string} data "G..." (or "M...") key representation to decode | |
| * @returns {Buffer} raw key | |
| */ | |
| static decodeEd25519PublicKey(data) { | |
| return decodeCheck('ed25519PublicKey', data); | |
| } | |
| /** | |
| * Returns true if the given Stellar public key is a valid ed25519 public key. | |
| * @param {string} publicKey public key to check | |
| * @returns {boolean} | |
| */ | |
| static isValidEd25519PublicKey(publicKey) { | |
| return isValid('ed25519PublicKey', publicKey); | |
| } | |
| /** | |
| * Encodes data to strkey ed25519 seed. | |
| * @param {Buffer} data data to encode | |
| * @returns {string} | |
| */ | |
| static encodeEd25519SecretSeed(data) { | |
| return encodeCheck('ed25519SecretSeed', data); | |
| } | |
| /** | |
| * Decodes strkey ed25519 seed to raw data. | |
| * @param {string} address data to decode | |
| * @returns {Buffer} | |
| */ | |
| static decodeEd25519SecretSeed(address) { | |
| return decodeCheck('ed25519SecretSeed', address); | |
| } | |
| /** | |
| * Returns true if the given Stellar secret key is a valid ed25519 secret seed. | |
| * @param {string} seed seed to check | |
| * @returns {boolean} | |
| */ | |
| static isValidEd25519SecretSeed(seed) { | |
| return isValid('ed25519SecretSeed', seed); | |
| } | |
| /** | |
| * Encodes data to strkey med25519 public key. | |
| * @param {Buffer} data data to encode | |
| * @returns {string} | |
| */ | |
| static encodeMed25519PublicKey(data) { | |
| return encodeCheck('med25519PublicKey', data); | |
| } | |
| /** | |
| * Decodes strkey med25519 public key to raw data. | |
| * @param {string} address data to decode | |
| * @returns {Buffer} | |
| */ | |
| static decodeMed25519PublicKey(address) { | |
| return decodeCheck('med25519PublicKey', address); | |
| } | |
| /** | |
| * Returns true if the given Stellar public key is a valid med25519 public key. | |
| * @param {string} publicKey public key to check | |
| * @returns {boolean} | |
| */ | |
| static isValidMed25519PublicKey(publicKey) { | |
| return isValid('med25519PublicKey', publicKey); | |
| } | |
| /** | |
| * Encodes data to strkey preAuthTx. | |
| * @param {Buffer} data data to encode | |
| * @returns {string} | |
| */ | |
| static encodePreAuthTx(data) { | |
| return encodeCheck('preAuthTx', data); | |
| } | |
| /** | |
| * Decodes strkey PreAuthTx to raw data. | |
| * @param {string} address data to decode | |
| * @returns {Buffer} | |
| */ | |
| static decodePreAuthTx(address) { | |
| return decodeCheck('preAuthTx', address); | |
| } | |
| /** | |
| * Encodes data to strkey sha256 hash. | |
| * @param {Buffer} data data to encode | |
| * @returns {string} | |
| */ | |
| static encodeSha256Hash(data) { | |
| return encodeCheck('sha256Hash', data); | |
| } | |
| /** | |
| * Decodes strkey sha256 hash to raw data. | |
| * @param {string} address data to decode | |
| * @returns {Buffer} | |
| */ | |
| static decodeSha256Hash(address) { | |
| return decodeCheck('sha256Hash', address); | |
| } | |
| /** | |
| * Encodes raw data to strkey signed payload (P...). | |
| * @param {Buffer} data data to encode | |
| * @returns {string} | |
| */ | |
| static encodeSignedPayload(data) { | |
| return encodeCheck('signedPayload', data); | |
| } | |
| /** | |
| * Decodes strkey signed payload (P...) to raw data. | |
| * @param {string} address address to decode | |
| * @returns {Buffer} | |
| */ | |
| static decodeSignedPayload(address) { | |
| return decodeCheck('signedPayload', address); | |
| } | |
| /** | |
| * Checks validity of alleged signed payload (P...) strkey address. | |
| * @param {string} address signer key to check | |
| * @returns {boolean} | |
| */ | |
| static isValidSignedPayload(address) { | |
| return isValid('signedPayload', address); | |
| } | |
| static getVersionByteForPrefix(address) { | |
| return strkeyTypes[address[0]]; | |
| } | |
| } | |
| /** | |
| * Sanity-checks whether or not a strkey *appears* valid. | |
| * | |
| * @param {string} versionByteName the type of strkey to expect in `encoded` | |
| * @param {string} encoded the strkey to validate | |
| * | |
| * @return {Boolean} whether or not the `encoded` strkey appears valid for the | |
| * `versionByteName` strkey type (see `versionBytes`, above). | |
| * | |
| * @note This isn't a *definitive* check of validity, but rather a best-effort | |
| * check based on (a) input length, (b) whether or not it can be decoded, | |
| * and (c) output length. | |
| */ | |
| function isValid(versionByteName, encoded) { | |
| if (!isString(encoded)) { | |
| return false; | |
| } | |
| // basic length checks on the strkey lengths | |
| switch (versionByteName) { | |
| case 'ed25519PublicKey': // falls through | |
| case 'ed25519SecretSeed': // falls through | |
| case 'preAuthTx': // falls through | |
| case 'sha256Hash': | |
| if (encoded.length !== 56) { | |
| return false; | |
| } | |
| break; | |
| case 'med25519PublicKey': | |
| if (encoded.length !== 69) { | |
| return false; | |
| } | |
| break; | |
| case 'signedPayload': | |
| if (encoded.length < 56 || encoded.length > 165) { | |
| return false; | |
| } | |
| break; | |
| default: | |
| return false; | |
| } | |
| let decoded = ''; | |
| try { | |
| decoded = decodeCheck(versionByteName, encoded); | |
| } catch (err) { | |
| return false; | |
| } | |
| // basic length checks on the resulting buffer sizes | |
| switch (versionByteName) { | |
| case 'ed25519PublicKey': // falls through | |
| case 'ed25519SecretSeed': // falls through | |
| case 'preAuthTx': // falls through | |
| case 'sha256Hash': | |
| return decoded.length === 32; | |
| case 'med25519PublicKey': | |
| return decoded.length === 40; // +8 bytes for the ID | |
| case 'signedPayload': | |
| return ( | |
| // 32 for the signer, +4 for the payload size, then either +4 for the | |
| // min or +64 for the max payload | |
| decoded.length >= 32 + 4 + 4 && decoded.length <= 32 + 4 + 64 | |
| ); | |
| default: | |
| return false; | |
| } | |
| } | |
| export function decodeCheck(versionByteName, encoded) { | |
| if (!isString(encoded)) { | |
| throw new TypeError('encoded argument must be of type String'); | |
| } | |
| const decoded = base32.decode(encoded); | |
| const versionByte = decoded[0]; | |
| const payload = decoded.slice(0, -2); | |
| const data = payload.slice(1); | |
| const checksum = decoded.slice(-2); | |
| if (encoded !== base32.encode(decoded)) { | |
| throw new Error('invalid encoded string'); | |
| } | |
| const expectedVersion = versionBytes[versionByteName]; | |
| if (isUndefined(expectedVersion)) { | |
| throw new Error( | |
| `${versionByteName} is not a valid version byte name. ` + | |
| `Expected one of ${Object.keys(versionBytes).join(', ')}` | |
| ); | |
| } | |
| if (versionByte !== expectedVersion) { | |
| throw new Error( | |
| `invalid version byte. expected ${expectedVersion}, got ${versionByte}` | |
| ); | |
| } | |
| const expectedChecksum = calculateChecksum(payload); | |
| if (!verifyChecksum(expectedChecksum, checksum)) { | |
| throw new Error(`invalid checksum`); | |
| } | |
| return Buffer.from(data); | |
| } | |
| export function encodeCheck(versionByteName, data) { | |
| if (isNull(data) || isUndefined(data)) { | |
| throw new Error('cannot encode null data'); | |
| } | |
| const versionByte = versionBytes[versionByteName]; | |
| if (isUndefined(versionByte)) { | |
| throw new Error( | |
| `${versionByteName} is not a valid version byte name. ` + | |
| `Expected one of ${Object.keys(versionBytes).join(', ')}` | |
| ); | |
| } | |
| data = Buffer.from(data); | |
| const versionBuffer = Buffer.from([versionByte]); | |
| const payload = Buffer.concat([versionBuffer, data]); | |
| const checksum = calculateChecksum(payload); | |
| const unencoded = Buffer.concat([payload, checksum]); | |
| return base32.encode(unencoded); | |
| } | |
| // Computes the CRC16-XModem checksum of `payload` in little-endian order | |
| function calculateChecksum(payload) { | |
| const checksum = Buffer.alloc(2); | |
| checksum.writeUInt16LE(crc.crc16xmodem(payload), 0); | |
| return checksum; | |
| } |