[website] Revert URL verification (#412)

* revert status-js

* revert website
This commit is contained in:
Felicio Mununga 2023-06-06 14:10:11 +02:00 committed by GitHub
parent 36538591ce
commit 8ec5bf3bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 155 additions and 393 deletions

View File

@ -21,17 +21,6 @@ export const ErrorPage = (props: Props) => {
</div> </div>
) )
// todo!: design review, not in designs
case ERROR_CODES.UNVERIFIED_CONTENT:
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-8 bg-white text-center">
<div className="h-[160px] w-[160px] rounded-full bg-[#ffd455]" />
<Text size={27} weight="semibold">
Unverified content.
</Text>
</div>
)
case ERROR_CODES.INTERNAL_SERVER_ERROR: case ERROR_CODES.INTERNAL_SERVER_ERROR:
default: default:
return ( return (

View File

@ -43,25 +43,25 @@ type Type = 'community' | 'channel' | 'profile'
type PreviewPageProps = { type PreviewPageProps = {
type: Type type: Type
unverifiedEncodedData?: string | null encodedData?: string | null
index?: boolean index?: boolean
} & ( } & (
| { | {
type: 'community' type: 'community'
unverifiedDecodedData?: ReturnType<typeof decodeCommunityURLData> | null decodedData?: ReturnType<typeof decodeCommunityURLData> | null
} }
| { | {
type: 'channel' type: 'channel'
unverifiedDecodedData?: ReturnType<typeof decodeChannelURLData> | null decodedData?: ReturnType<typeof decodeChannelURLData> | null
channelUuid?: string channelUuid?: string
} }
| { | {
type: 'profile' type: 'profile'
unverifiedDecodedData?: ReturnType<typeof decodeUserURLData> | null decodedData?: ReturnType<typeof decodeUserURLData> | null
} }
) )
export type VerifiedData = export type Data =
| { | {
type: 'community' type: 'community'
info: CommunityInfo info: CommunityInfo
@ -93,26 +93,26 @@ const JOIN_BUTTON_LABEL: Record<Type, string> = {
} }
export function PreviewPage(props: PreviewPageProps) { export function PreviewPage(props: PreviewPageProps) {
const { type, unverifiedDecodedData, unverifiedEncodedData } = props const { type, decodedData, encodedData } = props
const { asPath } = useRouter() const { asPath } = useRouter()
const toast = useToast() const toast = useToast()
// todo: default og image, not dynamic // todo: default og image, not dynamic
// const ogImageUrl = getOgImageUrl(props.unverifiedDecodedData) // const ogImageUrl = getOgImageUrl(props.decodedData)
// todo?: pass meta, info as component // todo?: pass meta, info as component
// todo?: pass image, color as props // todo?: pass image, color as props
const { const {
publicKey, publicKey,
channelUuid: urlChannelUuid, channelUuid: urlChannelUuid,
verifiedURLData, data: urlData,
errorCode: urlErrorCode, errorCode: urlErrorCode,
} = useURLData(type, unverifiedDecodedData, unverifiedEncodedData) } = useURLData(type, decodedData, encodedData)
const { const {
data: verifiedWakuData, data: wakuData,
isLoading, isLoading,
status, status,
refetch, refetch,
@ -120,7 +120,7 @@ export function PreviewPage(props: PreviewPageProps) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
queryKey: [type], queryKey: [type],
enabled: !!publicKey, enabled: !!publicKey,
queryFn: async function ({ queryKey }): Promise<VerifiedData | null> { queryFn: async function ({ queryKey }): Promise<Data | null> {
const client = await getRequestClient() const client = await getRequestClient()
switch (queryKey[0]) { switch (queryKey[0]) {
@ -173,36 +173,35 @@ export function PreviewPage(props: PreviewPageProps) {
return return
} }
if (verifiedURLData) { if (urlData) {
toast.custom('Information just updated', <InfoIcon size={20} />) toast.custom('Information just updated', <InfoIcon size={20} />)
} }
}, },
}) })
const loading = status === 'loading' || isLoading const loading = status === 'loading' || isLoading
const verifiedData: VerifiedData | undefined = const data: Data | undefined = wakuData ?? urlData
verifiedWakuData ?? verifiedURLData
const { avatarURL, bannerURL } = useMemo(() => { const { avatarURL, bannerURL } = useMemo(() => {
if (!verifiedData) { if (!data) {
return {} return {}
} }
const avatarURL = getAvatarURL(verifiedData) const avatarURL = getAvatarURL(data)
const bannerURL = getBannerURL(verifiedData) const bannerURL = getBannerURL(data)
return { avatarURL, bannerURL } return { avatarURL, bannerURL }
}, [verifiedData]) }, [data])
if (urlErrorCode) { if (urlErrorCode) {
return <ErrorPage errorCode={urlErrorCode} /> return <ErrorPage errorCode={urlErrorCode} />
} }
if (!loading && !verifiedData) { if (!loading && !data) {
return <ErrorPage errorCode={ERROR_CODES.NOT_FOUND} /> return <ErrorPage errorCode={ERROR_CODES.NOT_FOUND} />
} }
if ((loading && !verifiedData) || !verifiedData || !publicKey) { if ((loading && !data) || !data || !publicKey) {
return ( return (
<> <>
<div className="h-full xl:grid xl:grid-cols-[560px,auto]"> <div className="h-full xl:grid xl:grid-cols-[560px,auto]">
@ -317,7 +316,7 @@ export function PreviewPage(props: PreviewPageProps) {
{/* todo: theme; based on user system settings */} {/* todo: theme; based on user system settings */}
{/* todo: (system or both?) install banner */} {/* todo: (system or both?) install banner */}
<div <div
style={!bannerURL ? getGradientStyles(verifiedData) : undefined} style={!bannerURL ? getGradientStyles(data) : undefined}
className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]" className="relative h-full bg-gradient-to-b from-[var(--gradient-color)] to-[#fff] to-20% xl:grid xl:grid-cols-[560px,auto]"
> >
<div className="absolute left-0 right-0 top-0 xl:hidden"> <div className="absolute left-0 right-0 top-0 xl:hidden">
@ -336,54 +335,54 @@ export function PreviewPage(props: PreviewPageProps) {
{/* HERO */} {/* HERO */}
<div className="mb-[32px] xl:mb-[35px]"> <div className="mb-[32px] xl:mb-[35px]">
<div className="mb-2 xl:mb-4"> <div className="mb-2 xl:mb-4">
{verifiedData.type === 'community' && ( {data.type === 'community' && (
<Avatar <Avatar
type="community" type="community"
name={verifiedData.info.displayName} name={data.info.displayName}
src={avatarURL} src={avatarURL}
size={80} size={80}
/> />
)} )}
{verifiedData.type === 'channel' && ( {data.type === 'channel' && (
<Avatar <Avatar
type="channel" type="channel"
name={verifiedData.info.displayName} name={data.info.displayName}
emoji={verifiedData.info.emoji} emoji={data.info.emoji}
size={80} size={80}
// fixme: use `verifiedData.info.color` (e.g. #000000 format) // fixme: use `data.info.color` (e.g. #000000 format)
backgroundColor="$neutral-100" backgroundColor="$neutral-100"
/> />
)} )}
{verifiedData.type === 'profile' && ( {data.type === 'profile' && (
<Avatar <Avatar
type="user" type="user"
name={verifiedData.info.displayName} name={data.info.displayName}
src={avatarURL} src={avatarURL}
size={80} size={80}
colorHash={verifiedData.info.colorHash} colorHash={data.info.colorHash}
/> />
)} )}
</div> </div>
<h1 className="mb-3 text-[40px] font-semibold leading-[44px] xl:text-[64px] xl:leading-[68px]"> <h1 className="mb-3 text-[40px] font-semibold leading-[44px] xl:text-[64px] xl:leading-[68px]">
{verifiedData.type === 'channel' && '#'} {data.type === 'channel' && '#'}
{verifiedData.info.displayName} {data.info.displayName}
</h1> </h1>
<p className="mb-4 text-[15px] text-neutral-100 xl:text-[19px]"> <p className="mb-4 text-[15px] text-neutral-100 xl:text-[19px]">
{verifiedData.info.description} {data.info.description}
</p> </p>
{verifiedData.type === 'community' && ( {data.type === 'community' && (
<> <>
<div className="mb-4 flex items-center gap-1"> <div className="mb-4 flex items-center gap-1">
<MembersIcon size={20} color="$neutral-50" /> <MembersIcon size={20} color="$neutral-50" />
<Text size={15}> <Text size={15}>
{formatNumber(verifiedData.info.membersCount)} {formatNumber(data.info.membersCount)}
</Text> </Text>
</div> </div>
{verifiedData.info.tags?.length > 0 && ( {data.info.tags?.length > 0 && (
<div className="flex flex-wrap gap-[6px] xl:gap-[11px]"> <div className="flex flex-wrap gap-[6px] xl:gap-[11px]">
{verifiedData.info.tags.map(tag => ( {data.info.tags.map(tag => (
<Tag <Tag
key={tag.emoji + tag.text} key={tag.emoji + tag.text}
size={32} size={32}
@ -395,7 +394,7 @@ export function PreviewPage(props: PreviewPageProps) {
)} )}
</> </>
)} )}
{verifiedData.type === 'channel' && ( {data.type === 'channel' && (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Text size={13} color="$neutral-40"> <Text size={13} color="$neutral-40">
Channel in Channel in
@ -403,18 +402,18 @@ export function PreviewPage(props: PreviewPageProps) {
<ContextTag <ContextTag
type="community" type="community"
community={{ community={{
name: verifiedData.info.community.displayName, name: data.info.community.displayName,
src: getAvatarURL({ src: getAvatarURL({
type: 'community', type: 'community',
info: verifiedData.info.community as CommunityInfo, info: data.info.community as CommunityInfo,
}), }),
}} }}
/> />
</div> </div>
)} )}
{verifiedData.type === 'profile' && ( {data.type === 'profile' && (
<p className="text-[16px] tracking-[.2em]"> <p className="text-[16px] tracking-[.2em]">
{verifiedData.info.emojiHash} {data.info.emojiHash}
</p> </p>
)} )}
</div> </div>
@ -497,9 +496,7 @@ export function PreviewPage(props: PreviewPageProps) {
!bannerURL !bannerURL
? { ? {
backgroundColor: backgroundColor:
'color' in verifiedData.info 'color' in data.info ? data.info.color : neutral[100],
? verifiedData.info.color
: neutral[100],
} }
: undefined : undefined
} }
@ -527,14 +524,14 @@ const formatNumber = (n: number) => {
return formatter.format(n) return formatter.format(n)
} }
const getGradientStyles = (data: VerifiedData): CSSProperties => { const getGradientStyles = (data: Data): CSSProperties => {
return { return {
// @ts-expect-error CSSProperties do not handle inline CSS variables // @ts-expect-error CSSProperties do not handle inline CSS variables
'--gradient-color': 'color' in data.info ? data.info.color : neutral[100], '--gradient-color': 'color' in data.info ? data.info.color : neutral[100],
} }
} }
const getAvatarURL = (data: VerifiedData): string | undefined => { const getAvatarURL = (data: Data): string | undefined => {
let avatar: Uint8Array | undefined let avatar: Uint8Array | undefined
switch (data.type) { switch (data.type) {
case 'community': case 'community':
@ -560,7 +557,7 @@ const getAvatarURL = (data: VerifiedData): string | undefined => {
return url return url
} }
const getBannerURL = (data: VerifiedData): string | undefined => { const getBannerURL = (data: Data): string | undefined => {
let banner: Uint8Array | undefined let banner: Uint8Array | undefined
switch (data.type) { switch (data.type) {
case 'community': case 'community':

View File

@ -1,6 +1,5 @@
export const ERROR_CODES = { export const ERROR_CODES = {
NOT_FOUND: 404, NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500, INTERNAL_SERVER_ERROR: 500,
UNVERIFIED_CONTENT: 600,
INVALID_PUBLIC_KEY: 601, INVALID_PUBLIC_KEY: 601,
} }

View File

@ -7,13 +7,12 @@ import {
indicesToTags, indicesToTags,
publicKeyToColorHash, publicKeyToColorHash,
publicKeyToEmojiHash, publicKeyToEmojiHash,
verifyEncodedURLData, recoverPublicKeyFromEncodedURLData,
} from '@status-im/js' } from '@status-im/js'
import { decodeVerificationURLHash } from '@status-im/js/encode-url-hash'
import { ERROR_CODES } from '@/consts/error-codes' import { ERROR_CODES } from '@/consts/error-codes'
import type { VerifiedData } from '@/components/preview-page' import type { Data } from '@/components/preview-page'
import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js' import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js'
import type { import type {
decodeChannelURLData, decodeChannelURLData,
@ -23,17 +22,17 @@ import type {
export const useURLData = ( export const useURLData = (
type: 'community' | 'channel' | 'profile', type: 'community' | 'channel' | 'profile',
unverifiedDecodedData: decodedData:
| ReturnType<typeof decodeCommunityURLData> | ReturnType<typeof decodeCommunityURLData>
| ReturnType<typeof decodeChannelURLData> | ReturnType<typeof decodeChannelURLData>
| ReturnType<typeof decodeUserURLData> | ReturnType<typeof decodeUserURLData>
| undefined | undefined
| null, | null,
unverifiedEncodedData: string | undefined | null encodedData: string | undefined | null
) => { ) => {
const [publicKey, setPublicKey] = useState<string>() const [publicKey, setPublicKey] = useState<string>()
const [channelUuid, setChannelUuid] = useState<string>() const [channelUuid, setChannelUuid] = useState<string>()
const [info, setInfo] = useState<VerifiedData>() const [data, setData] = useState<Data>()
const [error, setError] = useState<keyof typeof ERROR_CODES>() const [error, setError] = useState<keyof typeof ERROR_CODES>()
const compressPublicKey = type !== 'profile' const compressPublicKey = type !== 'profile'
@ -46,9 +45,10 @@ export const useURLData = (
// return // return
// } // }
if (!unverifiedDecodedData || !unverifiedEncodedData) {
const hash = window.location.hash.replace('#', '') const hash = window.location.hash.replace('#', '')
// use provided public key
if (!decodedData || !encodedData) {
if (!hash) { if (!hash) {
setError('NOT_FOUND') setError('NOT_FOUND')
@ -61,37 +61,39 @@ export const useURLData = (
}) })
setPublicKey(publicKey) setPublicKey(publicKey)
return
} catch (error) { } catch (error) {
console.error(error) console.error(error)
setError('INVALID_PUBLIC_KEY') setError('INVALID_PUBLIC_KEY')
}
return return
} }
const hash = window.location.hash.replace('#', '')
const { signature, publicKey } = decodeVerificationURLHash(hash)
if (!signature || !publicKey) {
setError('UNVERIFIED_CONTENT')
return
} }
if (!verifyEncodedURLData(unverifiedEncodedData, hash)) { // recover public key
setError('UNVERIFIED_CONTENT') let deserializedPublicKey
try {
return const recoveredPublicKey = recoverPublicKeyFromEncodedURLData(
} encodedData,
hash
const deserializedPublicKey = deserializePublicKey(publicKey, { )
deserializedPublicKey = deserializePublicKey(recoveredPublicKey, {
compress: compressPublicKey, compress: compressPublicKey,
}) })
const verifiedDecodedData = unverifiedDecodedData setPublicKey(deserializedPublicKey)
} catch (error) {
console.error(error)
setError('INVALID_PUBLIC_KEY')
return
}
// map data
switch (type) { switch (type) {
case 'community': { case 'community': {
const data = verifiedDecodedData as Required< const data = decodedData as Required<
ReturnType<typeof decodeCommunityURLData> ReturnType<typeof decodeCommunityURLData>
> >
const info: CommunityInfo = { const info: CommunityInfo = {
@ -102,12 +104,12 @@ export const useURLData = (
tags: indicesToTags(data.tagIndices), tags: indicesToTags(data.tagIndices),
} }
setInfo({ type: 'community', info }) setData({ type: 'community', info })
break return
} }
case 'channel': { case 'channel': {
const data = verifiedDecodedData as Required< const data = decodedData as Required<
ReturnType<typeof decodeChannelURLData> ReturnType<typeof decodeChannelURLData>
> >
const info: Omit<ChannelInfo, 'community'> & { const info: Omit<ChannelInfo, 'community'> & {
@ -120,13 +122,13 @@ export const useURLData = (
community: { displayName: data.community.displayName }, community: { displayName: data.community.displayName },
} }
setInfo({ type: 'channel', info }) setData({ type: 'channel', info })
setChannelUuid(data.uuid) setChannelUuid(data.uuid)
break return
} }
case 'profile': { case 'profile': {
const data = verifiedDecodedData as Required< const data = decodedData as Required<
ReturnType<typeof decodeUserURLData> ReturnType<typeof decodeUserURLData>
> >
const info: UserInfo = { const info: UserInfo = {
@ -136,13 +138,11 @@ export const useURLData = (
emojiHash: publicKeyToEmojiHash(deserializedPublicKey), emojiHash: publicKeyToEmojiHash(deserializedPublicKey),
} }
setInfo({ type: 'profile', info }) setData({ type: 'profile', info })
break return
} }
} }
setPublicKey(deserializedPublicKey)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
setError('INTERNAL_SERVER_ERROR') setError('INTERNAL_SERVER_ERROR')
@ -152,7 +152,7 @@ export const useURLData = (
return { return {
publicKey, publicKey,
channelUuid, channelUuid,
verifiedURLData: info, data,
errorCode: error ? ERROR_CODES[error] : undefined, errorCode: error ? ERROR_CODES[error] : undefined,
} }
} }

View File

@ -15,8 +15,8 @@ export default function CommunityPreviewPage(
return ( return (
<PreviewPage <PreviewPage
type="community" type="community"
unverifiedDecodedData={props.unverifiedDecodedData} decodedData={props.decodedData}
unverifiedEncodedData={props.uverifiedEncodedData} encodedData={props.encodedData}
/> />
) )
} }

View File

@ -13,8 +13,8 @@ export default function ChannelPreviewPage(
return ( return (
<PreviewPage <PreviewPage
type="channel" type="channel"
unverifiedDecodedData={props.unverifiedDecodedData} decodedData={props.decodedData}
unverifiedEncodedData={props.uverifiedEncodedData} encodedData={props.encodedData}
channelUuid={props.channelUuid} channelUuid={props.channelUuid}
/> />
) )

View File

@ -13,8 +13,8 @@ export default function UserPreviewPage(
return ( return (
<PreviewPage <PreviewPage
type="profile" type="profile"
unverifiedDecodedData={props.unverifiedDecodedData} decodedData={props.decodedData}
unverifiedEncodedData={props.uverifiedEncodedData} encodedData={props.encodedData}
/> />
) )
} }

View File

@ -13,17 +13,13 @@ type DecodeType =
export type ServerSideProps<T = ReturnType<DecodeType>> = { export type ServerSideProps<T = ReturnType<DecodeType>> = {
/** /**
* For verifying on client without decoding or re-encoding. * For verifying on client without re-encoding.
*
* Verification in general is done on encoded data, so it is not
* decoded, decompressed and deserialized unnecessarily if not to be
* displayed or othwerwise needed.
*/ */
uverifiedEncodedData: string | null encodedData: string | null
/** /**
* For instaneous preview even if the data is not verified yet. * For instaneous preview of the content.
*/ */
unverifiedDecodedData: T | null decodedData: T | null
channelUuid?: string channelUuid?: string
} }
@ -46,8 +42,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
if (channelUuid) { if (channelUuid) {
const props: ServerSideProps = { const props: ServerSideProps = {
channelUuid: channelUuid[0], channelUuid: channelUuid[0],
uverifiedEncodedData: null, encodedData: null,
unverifiedDecodedData: null, decodedData: null,
} }
return { props } return { props }
@ -57,8 +53,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
if (!encodedData) { if (!encodedData) {
const props: ServerSideProps = { const props: ServerSideProps = {
uverifiedEncodedData: null, encodedData: null,
unverifiedDecodedData: null, decodedData: null,
} }
return { props } return { props }
@ -66,8 +62,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) {
const decodedData = decodeURLData(encodedData) const decodedData = decodeURLData(encodedData)
const props: ServerSideProps = { const props: ServerSideProps = {
uverifiedEncodedData: encodedData, encodedData,
unverifiedDecodedData: decodedData || null, decodedData: decodedData || null,
} }
// fixme: set Cache-Control // fixme: set Cache-Control

View File

@ -22,4 +22,4 @@ export { createRequestClient } from './request-client/request-client'
export { deserializePublicKey } from './utils/deserialize-public-key' export { deserializePublicKey } from './utils/deserialize-public-key'
export { publicKeyToColorHash } from './utils/public-key-to-color-hash' export { publicKeyToColorHash } from './utils/public-key-to-color-hash'
export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash' export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash'
export { verifyEncodedURLData } from './utils/sign-url-data' export { recoverPublicKeyFromEncodedURLData } from './utils/sign-url-data'

View File

@ -23,21 +23,11 @@ message User {
string color = 3; string color = 3;
} }
message Verification {
string signature = 1;
string public_key = 2;
}
message URLData { message URLData {
// Community, Channel, or User // Community, Channel, or User
bytes content = 1; bytes content = 1;
} }
message URLHash {
// Verification
bytes content = 1;
}
message URLParams { message URLParams {
string encoded_url_data = 1; string encoded_url_data = 1;
// Signature of encoded URL data // Signature of encoded URL data

View File

@ -267,61 +267,6 @@ export class User extends Message<User> {
} }
} }
/**
* @generated from message Verification
*/
export class Verification extends Message<Verification> {
/**
* @generated from field: string signature = 1;
*/
signature = ''
/**
* @generated from field: string public_key = 2;
*/
publicKey = ''
constructor(data?: PartialMessage<Verification>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime = proto3
static readonly typeName = 'Verification'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'signature', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
{ no: 2, name: 'public_key', kind: 'scalar', T: 9 /* ScalarType.STRING */ },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>
): Verification {
return new Verification().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>
): Verification {
return new Verification().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>
): Verification {
return new Verification().fromJsonString(jsonString, options)
}
static equals(
a: Verification | PlainMessage<Verification> | undefined,
b: Verification | PlainMessage<Verification> | undefined
): boolean {
return proto3.util.equals(Verification, a, b)
}
}
/** /**
* @generated from message URLData * @generated from message URLData
*/ */
@ -373,57 +318,6 @@ export class URLData extends Message<URLData> {
} }
} }
/**
* @generated from message URLHash
*/
export class URLHash extends Message<URLHash> {
/**
* Verification
*
* @generated from field: bytes content = 1;
*/
content = new Uint8Array(0)
constructor(data?: PartialMessage<URLHash>) {
super()
proto3.util.initPartial(data, this)
}
static readonly runtime = proto3
static readonly typeName = 'URLHash'
static readonly fields: FieldList = proto3.util.newFieldList(() => [
{ no: 1, name: 'content', kind: 'scalar', T: 12 /* ScalarType.BYTES */ },
])
static fromBinary(
bytes: Uint8Array,
options?: Partial<BinaryReadOptions>
): URLHash {
return new URLHash().fromBinary(bytes, options)
}
static fromJson(
jsonValue: JsonValue,
options?: Partial<JsonReadOptions>
): URLHash {
return new URLHash().fromJson(jsonValue, options)
}
static fromJsonString(
jsonString: string,
options?: Partial<JsonReadOptions>
): URLHash {
return new URLHash().fromJsonString(jsonString, options)
}
static equals(
a: URLHash | PlainMessage<URLHash> | undefined,
b: URLHash | PlainMessage<URLHash> | undefined
): boolean {
return proto3.util.equals(URLHash, a, b)
}
}
/** /**
* @generated from message URLParams * @generated from message URLParams
*/ */

View File

@ -66,7 +66,7 @@ describe('Create URLs', () => {
) )
).toString() ).toString()
).toBe( ).toBe(
'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#Co0BClhRbk8yaHc1dFZBRS1NRDVpOE1xNHNfb0dXZDByUkZtbE9iZ1JVTlFYdFVOd1AxaXhGdzkxNFk0LUJRcEYwOEtPcXBhVUxDaDdVQ3RsV1ItTzBZUDhNd0E9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx' 'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#QnO2hw5tVAE-MD5i8Mq4s_oGWd0rRFmlObgRUNQXtUNwP1ixFw914Y4-BQpF08KOqpaULCh7UCtlWR-O0YP8MwA='
) )
}) })
@ -122,7 +122,7 @@ describe('Create URLs', () => {
) )
).toString() ).toString()
).toBe( ).toBe(
'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#Co0BClg3YWVCLU02cElidnBTVkdNNFRlSmtLV1B5YTRZUkFIYnE0YW1MMGNIbFNCcFJLbjdfbHlSNGt4RURvMmhDNGtvcVBXWWVfYWsyUjljU1ZLU2lWX25OQUE9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx' 'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#7aeB-M6pIbvpSVGM4TeJkKWPya4YRAHbq4amL0cHlSBpRKn7_lyR4kxEDo2hC4koqPWYe_ak2R9cSVKSiV_nNAA='
) )
}) })
@ -159,7 +159,7 @@ describe('Create URLs', () => {
) )
).toString() ).toString()
).toBe( ).toBe(
'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#Co0BClhMYlFVZEpESENLb2k4RHpvWXlYODlicEtyVGpWVjNTaHFIM0U2NGJEaWZKQjJHa2VkdExCZlZLQTAyUmJVZlgwNzRwYjlpM293R3dSZFM2eF9udHhyUUE9EjF6UTNzaHdRUGhSdURKU2pWR1ZCblRqQ2RnWHk1aTlXUWFlVlBkR0pENnlUYXJKUVNq' 'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#LbQUdJDHCKoi8DzoYyX89bpKrTjVV3ShqH3E64bDifJB2GkedtLBfVKA02RbUfX074pb9i3owGwRdS6x_ntxrQA='
) )
}) })
}) })

View File

@ -19,14 +19,12 @@ export async function createCommunityURLWithData(
communityPrivateKey: Uint8Array | string communityPrivateKey: Uint8Array | string
): Promise<URL> { ): Promise<URL> {
const encodedURLData = encodeCommunityURLData(communityData) const encodedURLData = encodeCommunityURLData(communityData)
const encodedVerificationURLHash = await signEncodedURLData( const encodedSignature = await signEncodedURLData(
encodedURLData, encodedURLData,
communityPrivateKey communityPrivateKey
) )
return new URL( return new URL(`${BASE_URL}/c/${encodedURLData}#${encodedSignature}`)
`${BASE_URL}/c/${encodedURLData}#${encodedVerificationURLHash}`
)
} }
export function createChannelURLWithChatKey( export function createChannelURLWithChatKey(
@ -41,14 +39,12 @@ export async function createChannelURLWithData(
communityPrivateKey: Uint8Array | string communityPrivateKey: Uint8Array | string
): Promise<URL> { ): Promise<URL> {
const encodedURLData = encodeChannelURLData(channelData) const encodedURLData = encodeChannelURLData(channelData)
const encodedVerificationURLHash = await signEncodedURLData( const encodedSignature = await signEncodedURLData(
encodedURLData, encodedURLData,
communityPrivateKey communityPrivateKey
) )
return new URL( return new URL(`${BASE_URL}/cc/${encodedURLData}#${encodedSignature}`)
`${BASE_URL}/cc/${encodedURLData}#${encodedVerificationURLHash}`
)
} }
export function createUserURLWithENS(ensName: string): URL { export function createUserURLWithENS(ensName: string): URL {
@ -64,12 +60,10 @@ export async function createUserURLWithData(
userPrivateKey: Uint8Array | string userPrivateKey: Uint8Array | string
): Promise<URL> { ): Promise<URL> {
const encodedURLData = encodeUserURLData(userData) const encodedURLData = encodeUserURLData(userData)
const encodedVerificationURLHash = await signEncodedURLData( const encodedSignature = await signEncodedURLData(
encodedURLData, encodedURLData,
userPrivateKey userPrivateKey
) )
return new URL( return new URL(`${BASE_URL}/u/${encodedURLData}#${encodedSignature}`)
`${BASE_URL}/u/${encodedURLData}#${encodedVerificationURLHash}`
)
} }

View File

@ -1,24 +0,0 @@
import { describe, expect, test } from 'vitest'
import {
decodeVerificationURLHash,
encodeVerificationURLHash,
} from './encode-url-hash'
describe('Encode URL hash', () => {
test('should encode and decode verification hash', () => {
const data = {
signature:
'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=', // not generated by the pk
publicKey: 'zQ3shUHp2rAM1yqBYeo6LhFbtrozG5mZeA6cRoGohsudtsieT',
}
const encodedHash = encodeVerificationURLHash(data)
const decodedHash = decodeVerificationURLHash(encodedHash)
expect(encodedHash).toBe(
'Co0BClhrLW43ZC05UGN4Nmh0ODdGNHJpUDV4QXcxdjdTLWUxSEdNUmFlYU8wNjhRM0lGMUpvOHhPeWVNVDlZcjNXdjM0OVoyQ2RCenlsdzhNODNDZ1FoY01vZ0E9EjF6UTNzaFVIcDJyQU0xeXFCWWVvNkxoRmJ0cm96RzVtWmVBNmNSb0dvaHN1ZHRzaWVU'
)
expect(decodedHash).toEqual(data)
})
})

View File

@ -1,41 +0,0 @@
import { base64url } from '@scure/base'
import { URLHash, Verification } from '../protos/url_pb'
import type { PlainMessage } from '@bufbuild/protobuf'
export type EncodedVerificationURLHash = string & {
_: 'EncodedVerificationURLHash'
}
export function encodeVerificationURLHash(
data: PlainMessage<Verification>
): EncodedVerificationURLHash {
return encodeURLHash(
new Verification(data).toBinary()
) as EncodedVerificationURLHash
}
export function decodeVerificationURLHash(
data: string
): PlainMessage<Verification> {
const deserialized = decodeURLHash(data)
return Verification.fromBinary(
deserialized.content
).toJson() as PlainMessage<Verification>
}
function encodeURLHash(data: Uint8Array): string {
const serialized = new URLHash({ content: data }).toBinary()
const encoded = base64url.encode(serialized)
return encoded
}
function decodeURLHash(data: string): URLHash {
const decoded = base64url.decode(data)
const deserialized = URLHash.fromBinary(decoded)
return deserialized
}

View File

@ -1,7 +1,9 @@
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import { encodeVerificationURLHash } from './encode-url-hash' import {
import { signEncodedURLData, verifyEncodedURLData } from './sign-url-data' recoverPublicKeyFromEncodedURLData,
signEncodedURLData,
} from './sign-url-data'
import type { EncodedURLData } from './encode-url-data' import type { EncodedURLData } from './encode-url-data'
@ -13,50 +15,41 @@ const publicKey =
'0x04f9134866f2bd8f45f2bc7893c95a6b989378c370088c9a1a5a53eda2ebb8a1e8386921592b6bd56fc3573f03c46df3396cc42e2993cdc001855c858865d768a7' '0x04f9134866f2bd8f45f2bc7893c95a6b989378c370088c9a1a5a53eda2ebb8a1e8386921592b6bd56fc3573f03c46df3396cc42e2993cdc001855c858865d768a7'
const encodedURLData = const encodedURLData =
'G74AgK0ObFNmYT-WC_Jcc9KfSjHXAQo9THKEEbgPaJoItceMES-bUxr2Tj9efv447rRefBIUg9CEsSFyjBOFTRdZ9PH2wUOW8hVNYqIje3BC96mZ8uFogqM6k7gCCJnMHy4ulsmsgHTdeh5dAzTNNuG8m9XB8oVeildTCKlRhINnTZh4kAl5sP8SzBB4V2_I41a8PKl3mcS0z_eF5gA=' as EncodedURLData 'G74AgK0ObFNmYT-WC_Jcc9KfSjHXAQo9THKEEbgPaJoItceMES-bUxr2Tj9efv447rRefBIUg9CEsSFyjBOFTRdZ9PH2wUOW8hVNYqIje3BC96mZ8uFogqM6k7gCCJnMHy4ulsmsgHTdeh5dAzTNNuG8m9XB8oVeildTCKlRhINnTZh4kAl5sP8SzBB4V2_I41a8PKl3mcS0z_eF5gA=' as EncodedURLData
const encodedSignature =
test('should verify URL data and correspoinding signature', async () => {
const encodedVerificationURLHash = await signEncodedURLData(
encodedURLData,
privateKey
)
expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe(
true
)
})
test('should not verify URL data and random signature', async () => {
const randomSignature =
'OyOgY6Zta8S7U4l5Bv_9E_7snALhixwvjxORVAVJ-YJk-tMSGgstOy5XEEQx25TQJIAtpWf8eHnEmV8V-GmouQA='
const encodedVerificationURLHash = encodeVerificationURLHash({
signature: randomSignature,
publicKey,
})
expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe(
false
)
// see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968
// expect(verifyEncodedURLData(randomSignature, encodedURLData)).toBe(false)
})
test('should not verify random URL data and random signature', async () => {
const randomEncodedURLData =
'CyeACk0KHkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBlZ2VzdGFzLhIYV2UgZG8gbm90IHN1cHBvcnQgQWxpY2UuGMCEPSIHIzQzNjBERioEAQIDBAM=' as EncodedURLData
const randomSignature =
'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=' 'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA='
const encodedVerificationURLHash = encodeVerificationURLHash({ test('should sign URL data', async () => {
signature: randomSignature, expect(await signEncodedURLData(encodedURLData, privateKey)).toBe(
publicKey, encodedSignature
}) )
})
test('should recover original public key from URL data', async () => {
expect(
await recoverPublicKeyFromEncodedURLData(encodedURLData, encodedSignature)
).toBe(publicKey)
})
test('should not recover original public key from same URL data but changed signature', async () => {
const changedEncodedSignature =
'OyOgY6Zta8S7U4l5Bv_9E_7snALhixwvjxORVAVJ-YJk-tMSGgstOy5XEEQx25TQJIAtpWf8eHnEmV8V-GmouQA='
expect( expect(
verifyEncodedURLData(randomEncodedURLData, encodedVerificationURLHash) await recoverPublicKeyFromEncodedURLData(
).toBe(false) encodedURLData,
// see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968 changedEncodedSignature
// expect(verifyEncodedURLData(randomSignature, randomEncodedURLData)).toBe( )
// false ).not.toBe(publicKey)
// ) })
test('should not recover original public key from same signature but changed URL data', async () => {
const changedEncodedURLData =
'CyeACk0KHkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBlZ2VzdGFzLhIYV2UgZG8gbm90IHN1cHBvcnQgQWxpY2UuGMCEPSIHIzQzNjBERioEAQIDBAM=' as EncodedURLData
expect(
await recoverPublicKeyFromEncodedURLData(
changedEncodedURLData,
encodedSignature
)
).not.toBe(publicKey)
}) })

View File

@ -1,51 +1,30 @@
import { base64url } from '@scure/base' import { base64url } from '@scure/base'
import { getPublicKey } from 'ethereum-cryptography/secp256k1' import { toHex, utf8ToBytes as toBytes } from 'ethereum-cryptography/utils'
import { bytesToHex } from 'ethereum-cryptography/utils'
import { deserializePublicKey } from './deserialize-public-key' import { recoverPublicKey } from './recover-public-key'
import { import { signData } from './sign-data'
decodeVerificationURLHash,
encodeVerificationURLHash,
} from './encode-url-hash'
import { serializePublicKey } from './serialize-public-key'
import { signData, verifySignedData } from './sign-data'
import type { EncodedURLData } from './encode-url-data' import type { EncodedURLData } from './encode-url-data'
import type { EncodedVerificationURLHash } from './encode-url-hash'
export async function signEncodedURLData( export async function signEncodedURLData(
encodedURLData: EncodedURLData, encodedURLData: EncodedURLData,
privateKey: Uint8Array | string privateKey: Uint8Array | string
): Promise<EncodedVerificationURLHash> { ): Promise<string> {
const signature = await signData(encodedURLData, privateKey) const signature = await signData(encodedURLData, privateKey)
const encodedSignature = base64url.encode(signature) const encodedSignature = base64url.encode(signature)
const serializedPublicKey = serializePublicKey(
`0x${bytesToHex(getPublicKey(privateKey))}`
)
return encodeVerificationURLHash({ return encodedSignature
signature: encodedSignature,
publicKey: serializedPublicKey,
})
} }
export function verifyEncodedURLData( export function recoverPublicKeyFromEncodedURLData(
encodedURLData: string, encodedURLData: string,
encodedVerificationURLHash: string encodedSignature: string
): boolean { ): string {
const { signature, publicKey } = decodeVerificationURLHash( const decodedSignature = base64url.decode(encodedSignature)
encodedVerificationURLHash const recoveredPublicKey = recoverPublicKey(
)
const decodedSignature = base64url.decode(signature)
const deserializedPublicKey = deserializePublicKey(publicKey, {
compress: false,
})
return verifySignedData(
decodedSignature, decodedSignature,
encodedURLData, toBytes(encodedURLData)
deserializedPublicKey
) )
return `0x${toHex(recoveredPublicKey)}`
} }

View File

@ -26,11 +26,7 @@ export default defineConfig(({ mode }) => {
build: { build: {
target: 'es2020', target: 'es2020',
lib: { lib: {
entry: [ entry: ['./src/index.ts', './src/utils/encode-url-data.ts'],
'./src/index.ts',
'./src/utils/encode-url-data.ts',
'./src/utils/encode-url-hash.ts',
],
fileName: (format, entryName) => { fileName: (format, entryName) => {
if (!['es'].includes(format)) { if (!['es'].includes(format)) {
throw new Error(`Unexpected format: ${format}`) throw new Error(`Unexpected format: ${format}`)