Update project structure and build process

This commit is contained in:
Juan Pablo Civile
2025-05-13 11:10:08 -03:00
parent 124e9fa1bc
commit d9f3e925a4
277 changed files with 15321 additions and 930 deletions

View File

@@ -0,0 +1,357 @@
package emergencykit
type pageData struct {
Css string
Content string
}
type contentData struct {
FirstEncryptedKey string
SecondEncryptedKey string
VerificationCode string
CurrentDate string
Descriptors string
IconHelp string
IconPadlock string
}
const page = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Emergency Kit</title>
<style>
{{.Css}}
</style>
</head>
<body>
{{.Content}}
</body>
</html>
`
const contentEN = `
<header>
<h1>Emergency Kit</h1>
<h2>Verification <span class="verification-code">#{{.VerificationCode}}</span></p>
</header>
<div class="backup">
<div class="intro">
{{.IconPadlock}}
<div class="text">
<h1>Encrypted backup</h1>
<h2>It can only be decrypted using your <strong>Recovery Code</strong>.</h2>
</div>
</div>
<div class="keys">
<div class="key">
<h3>First key</h3>
<p>{{.FirstEncryptedKey}}</p>
</div>
<div class="key">
<h3>Second key</h3>
<p>{{.SecondEncryptedKey}}</p>
</div>
<div class="date">
Created on <date>{{.CurrentDate}}</date>
</div>
</div>
</div>
<section class="instructions">
<h1>Instructions</h1>
<p>This emergency procedure will help you recover your funds if you are unable to use Muun on your phone.</p>
<div class="item">
<div class="number-box">
<div class="number">1</div>
</div>
<div class="text-box">
<h3>Find your Recovery Code</h3>
<p>You wrote this code on paper before creating your Emergency Kit. Youll need it later.</p>
</div>
</div>
<div class="item">
<div class="number-box">
<div class="number">2</div>
</div>
<div class="text-box">
<h3>Download the Recovery Tool</h3>
<p>Go to <a href="https://github.com/muun/recovery">github.com/muun/recovery</a> and download the tool on your computer.</p>
</div>
</div>
<div class="item">
<div class="number-box">
<div class="number">3</div>
</div>
<div class="text-box">
<h3>Recover your funds</h3>
<p>Run the Recovery Tool and follow the steps. It will safely transfer your funds to a Bitcoin address that you
choose.</p>
</div>
</div>
</section>
<section class="help">
{{.IconHelp}}
<div class="text-box">
<h3>Need help?</h3>
<p>
Contact us at <a href="mailto:support@muun.com">support@muun.com</a>. Were always there to help.
</p>
</div>
</section>
<section class="advanced page-break-before">
<h1>Advanced information</h1>
<h2>Output descriptors</h2>
<p>These descriptors, combined with your keys, specify how to locate your wallets funds on the Bitcoin blockchain.</p>
{{ if .Descriptors }}
{{.Descriptors}}
{{ else }}
<ul class="descriptors">
<!-- These lines are way too long, but dividing them introduces unwanted spaces -->
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/0/*, <span class="fp">second key</span>/1'/1'/0/*)))</li>
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/1/*, <span class="fp">second key</span>/1'/1'/1/*)))</li>
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/2/*/*, <span class="fp">second key</span>/1'/1'/2/*/*)))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/0/*, <span class="fp">second key</span>/1'/1'/0/*))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/1/*, <span class="fp">second key</span>/1'/1'/1/*))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">first key</span>/1'/1'/2]/*/*, <span class="fp">second key</span>/1'/1'/2/*/*))</li>
</ul>
{{ end }}
<p>
Output descriptors are part of a developing standard for Recovery that Muun intends to support and is helping grow.
Since the standard is in a very early stage, the list above includes some non-standard elements.
</p>
<p>
When descriptors reach a more mature stage, youll be able to take your funds from one wallet to another with
complete independence. Muun believes this freedom is at the core of Bitcoins promise, and is working towards
that goal.
</p>
</section>
`
const contentES = `
<header>
<h1>Kit de Emergencia</h1>
<h2>Verificación <span class="verification-code">#{{.VerificationCode}}</span></p>
</header>
<div class="backup">
<div class="intro">
{{.IconPadlock}}
<div class="text">
<h1>Respaldo encriptado</h1>
<h2>Sólo puede ser desencriptado con tu <strong>Código de Recuperación</strong>.</h2>
</div>
</div>
<div class="keys">
<div class="key">
<h3>Primera clave</h3>
<p>{{.FirstEncryptedKey}}</p>
</div>
<div class="key">
<h3>Segunda clave</h3>
<p>{{.SecondEncryptedKey}}</p>
</div>
<div class="date">
Creado el <date>{{.CurrentDate}}</date>
</div>
</div>
</div>
<section class="instructions">
<h1>Instrucciones</h1>
<p>Éste procedimiento de emergencia te ayudará a recuperar tus fondos si no puedes usar Muun en tu teléfono.</p>
<div class="item">
<div class="number-box">
<div class="number">1</div>
</div>
<div class="text-box">
<h3>Encuentra tu Código de Recuperación</h3>
<p>Lo escribiste en papel antes de crear tu Kit de Emergencia. Lo necesitarás después.</p>
</div>
</div>
<div class="item">
<div class="number-box">
<div class="number">2</div>
</div>
<div class="text-box">
<h3>Descarga la Herramienta de Recuperación</h3>
<p>Ingresa en <a href="github.com/muun/recovery">github.com/muun/recovery</a> y descarga la herramienta en tu computadora..</p>
</div>
</div>
<div class="item">
<div class="number-box">
<div class="number">3</div>
</div>
<div class="text-box">
<h3>Recupera tus fondos</h3>
<p>Ejecuta la Herramienta de Recuperación y sigue los pasos. Transferirá tus fondos a una dirección de Bitcoin que elijas.</p>
</div>
</div>
</section>
<section class="help">
{{.IconHelp}}
<div class="text-box">
<h3>¿Necesitas ayuda?</h3>
<p>
Contáctanos en <a href="mailto:support@muun.com">support@muun.com</a>. Siempre estamos disponibles para ayudar.
</p>
</div>
</section>
<section class="advanced page-break-before">
<h1>Información Avanzada</h1>
<h2>Output descriptors</h2>
<p>Estos descriptors, combinados con tus claves, indican cómo encontrar los fondos de tu billetera en la blockchain de Bitcoin.</p>
{{ if .Descriptors }}
{{.Descriptors}}
{{ else }}
<ul class="descriptors">
<!-- These lines are way too long, but dividing them introduces unwanted spaces -->
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/0/*, <span class="fp">segunda clave</span>/1'/1'/0/*)))</li>
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/1/*, <span class="fp">segunda clave</span>/1'/1'/1/*)))</li>
<li><span class="f">sh</span>(<span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/2/*/*, <span class="fp">segunda clave</span>/1'/1'/2/*/*)))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/0/*, <span class="fp">segunda clave</span>/1'/1'/0/*))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/1/*, <span class="fp">segunda clave</span>/1'/1'/1/*))</li>
<li><span class="f">wsh</span>(<span class="f">multi</span>(2, <span class="fp">primera clave</span>/1'/1'/2]/*/*, <span class="fp">segunda clave</span>/1'/1'/2/*/*))</li>
</ul>
{{ end }}
<p>
Los output descriptors son parte de un estándar de recuperación actualmente en desarrollo. Muun tiene la intención
de soportar este estándar y apoyar su crecimiento. Dado que se encuentra en una etapa muy temprana, la siguiente lista
incluye algunos elementos que aún no están estandarizados.
</p>
<p>
Cuando los descriptors lleguen a una etapa más madura, podrás llevar tus fondos de una billetera a la otra con completa
independencia. Muun cree que ésta libertad es central a la promesa de Bitcoin, y está trabajando para que eso suceda.
</p>
</section>
`
const iconHelp = `
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g filter="url(#filter0_d)">
<circle cx="36" cy="36" r="28" fill="white"/>
</g>
<path d="M51.9762 41.7833L51.9999 28.1164C52.005 27.3149 51.8507 26.5203 51.5461 25.7789C51.2414 25.0374 50.7924 24.3638 50.2252 23.7972C49.6572 23.2232 48.9802 22.7686 48.2338 22.4599C47.4874 22.1513 46.6869 21.995 45.8791 22.0001H26.1208C24.4981 22.0022 22.9424 22.6473 21.795 23.7938C20.6476 24.9404 20.0021 26.4949 20 28.1164V41.2789C20.0021 42.9004 20.6476 44.4548 21.795 45.6014C22.9424 46.748 24.4981 47.393 26.1208 47.3951H26.5625V51.1547C26.5578 51.7232 26.7257 52.2798 27.0439 52.7512C27.3621 53.2225 27.8158 53.5865 28.3451 53.7951C28.6816 53.9281 29.04 53.9976 29.402 54C29.7741 53.9998 30.1425 53.9251 30.4852 53.7803C30.828 53.6354 31.1382 53.4234 31.3975 53.1567L36.6901 47.3951L48.1895 46.2128L51.9762 41.7833ZM35.8698 45.3774L29.7175 51.4778C29.6594 51.5449 29.5815 51.5917 29.495 51.6116C29.4085 51.6314 29.3179 51.6232 29.2363 51.5882C29.1493 51.5551 29.075 51.4953 29.024 51.4175C28.973 51.3396 28.948 51.2476 28.9524 51.1547V46.2128C28.9524 45.8993 28.8277 45.5986 28.6059 45.3769C28.384 45.1551 28.083 45.0306 27.7693 45.0306H26.1208C25.125 45.0306 24.17 44.6353 23.4659 43.9317C22.7618 43.2281 22.3663 42.2739 22.3663 41.2789V28.1164C22.3663 27.1213 22.7618 26.1671 23.4659 25.4635C24.17 24.7599 25.125 24.3646 26.1208 24.3646H45.8791C46.372 24.3641 46.86 24.4614 47.315 24.6508C47.7699 24.8402 48.1827 25.118 48.5293 25.4681C48.8797 25.8145 49.1577 26.227 49.3472 26.6816C49.5368 27.1362 49.6341 27.6239 49.6336 28.1164V41.2789C49.6336 42.2739 49.238 43.2281 48.5339 43.9317C47.8298 44.6353 46.8749 45.0306 45.8791 45.0306H36.6901C36.3764 45.0309 36.0757 45.1556 35.854 45.3774H35.8698ZM36.6901 47.3951H45.8791C47.4141 47.3926 48.8922 46.8146 50.0211 45.7755C51.1501 44.7364 51.8478 43.3117 51.9762 41.7833L48.1895 46.2128L36.6901 47.3951Z" fill="#2474CD"/>
<path d="M34.708 37.1242C34.612 37.1242 34.528 37.0942 34.456 37.034C34.384 36.9619 34.348 36.8777 34.348 36.7815V36.3666C34.432 35.8615 34.618 35.4105 34.906 35.0136C35.206 34.6168 35.614 34.1658 36.13 33.6607C36.514 33.2758 36.802 32.9631 36.994 32.7226C37.186 32.4701 37.288 32.2175 37.3 31.965C37.336 31.5922 37.21 31.2975 36.922 31.081C36.646 30.8525 36.31 30.7383 35.914 30.7383C34.978 30.7383 34.402 31.1893 34.186 32.0912C34.09 32.3799 33.904 32.5242 33.628 32.5242H31.432C31.3 32.5242 31.192 32.4821 31.108 32.3979C31.036 32.3017 31 32.1814 31 32.0371C31.024 31.3757 31.234 30.7503 31.63 30.161C32.026 29.5597 32.608 29.0727 33.376 28.6998C34.144 28.327 35.062 28.1406 36.13 28.1406C37.222 28.1406 38.11 28.315 38.794 28.6638C39.478 29.0005 39.964 29.4214 40.252 29.9265C40.552 30.4196 40.702 30.9247 40.702 31.4418C40.702 32.0311 40.564 32.5482 40.288 32.9932C40.024 33.4382 39.628 33.9493 39.1 34.5266C38.776 34.8753 38.518 35.17 38.326 35.4105C38.146 35.651 38.008 35.9036 37.912 36.1681C37.876 36.2764 37.834 36.4387 37.786 36.6552C37.69 36.8236 37.606 36.9438 37.534 37.016C37.462 37.0881 37.36 37.1242 37.228 37.1242H34.708ZM34.744 40.9486C34.612 40.9486 34.504 40.9065 34.42 40.8223C34.336 40.7381 34.294 40.6299 34.294 40.4976V38.4411C34.294 38.3088 34.336 38.2006 34.42 38.1164C34.504 38.0322 34.612 37.9901 34.744 37.9901H37.048C37.18 37.9901 37.288 38.0322 37.372 38.1164C37.468 38.2006 37.516 38.3088 37.516 38.4411V40.4976C37.516 40.6299 37.468 40.7381 37.372 40.8223C37.288 40.9065 37.18 40.9486 37.048 40.9486H34.744Z" fill="#182449"/>
</g>
<defs>
<filter id="filter0_d" x="0" y="4" width="72" height="72" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="4"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.124943 0 0 0 0 0.228158 0 0 0 0 0.346117 0 0 0 0.05 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<clipPath id="clip0">
<rect width="72" height="72" fill="white"/>
</clipPath>
</defs>
</svg>
`
const iconPadlock = `
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<g filter="url(#filter0_dd)">
<g filter="url(#filter1_i)">
<path d="M48.7367 30.2734H23.2633C21.461 30.2734 20 31.7345 20 33.5367V53.192C20 54.9942 21.461 56.4553 23.2633 56.4553H48.7367C50.539 56.4553 52 54.9942 52 53.192V33.5367C52 31.7345 50.539 30.2734 48.7367 30.2734Z" fill="url(#paint0_linear)"/>
</g>
<path d="M38.9119 41.1786C38.9119 42.7853 37.6095 44.0877 36.0028 44.0877C34.3962 44.0877 33.0938 42.7853 33.0938 41.1786C33.0938 39.572 34.3962 38.2695 36.0028 38.2695C37.6095 38.2695 38.9119 39.572 38.9119 41.1786Z" fill="url(#paint1_radial)"/>
<path d="M34.4106 43.9113C34.4915 43.5876 34.7824 43.3604 35.1161 43.3604H36.8895C37.2233 43.3604 37.5142 43.5876 37.5951 43.9113L38.686 48.275C38.8008 48.734 38.4536 49.1786 37.9805 49.1786H34.0252C33.5521 49.1786 33.2049 48.734 33.3197 48.275L34.4106 43.9113Z" fill="url(#paint2_radial)"/>
<g filter="url(#filter2_i)">
<path d="M25.0906 24.8182V30.2727H29.0906V24.8182C29.0906 22.8788 30.4724 19 35.9997 19C41.5269 19 42.9088 22.8788 42.9088 24.8182V30.2727H46.9088V24.8182C46.9088 21.5455 44.7269 15 35.9997 15C27.2724 15 25.0906 21.5455 25.0906 24.8182Z" fill="#2573F7"/>
<path d="M25.0906 24.8182V30.2727H29.0906V24.8182C29.0906 22.8788 30.4724 19 35.9997 19C41.5269 19 42.9088 22.8788 42.9088 24.8182V30.2727H46.9088V24.8182C46.9088 21.5455 44.7269 15 35.9997 15C27.2724 15 25.0906 21.5455 25.0906 24.8182Z" fill="url(#paint3_linear)"/>
</g>
</g>
</g>
<defs>
<filter id="filter0_dd" x="7.99957" y="8.99978" width="56.0009" height="65.4561" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="6.00022"/>
<feGaussianBlur stdDeviation="6.00022"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.340702 0 0 0 0 0.386926 0 0 0 0 0.529451 0 0 0 0.3 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="1.50005"/>
<feGaussianBlur stdDeviation="1.50005"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
</filter>
<filter id="filter1_i" x="19.5921" y="29.4576" width="32.4079" height="26.9976" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-0.40791" dy="-0.815819"/>
<feGaussianBlur stdDeviation="0.815819"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.25098 0 0 0 0 0.380392 0 0 0 0 0.552941 0 0 0 1 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow"/>
</filter>
<filter id="filter2_i" x="24.7156" y="14.625" width="22.1932" height="15.6477" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dx="-0.375014" dy="-0.375014"/>
<feGaussianBlur stdDeviation="0.375014"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow"/>
</filter>
<linearGradient id="paint0_linear" x1="25.8754" y1="40.3205" x2="64.6733" y2="80.0278" gradientUnits="userSpaceOnUse">
<stop stop-color="#91ACC9"/>
<stop offset="0.561326" stop-color="#3D5F8C"/>
</linearGradient>
<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(36.0028 43.7241) rotate(90) scale(5.45455 2.90909)">
<stop stop-color="#0B141D"/>
<stop offset="1" stop-color="#27394D"/>
</radialGradient>
<radialGradient id="paint2_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(36.0028 43.7241) rotate(90) scale(5.45455 2.90909)">
<stop stop-color="#0B141D"/>
<stop offset="1" stop-color="#27394D"/>
</radialGradient>
<linearGradient id="paint3_linear" x1="35.9997" y1="26.0114" x2="35.9997" y2="34.4318" gradientUnits="userSpaceOnUse">
<stop stop-color="#435F7D"/>
<stop offset="1" stop-color="#213953"/>
</linearGradient>
<clipPath id="clip0">
<rect width="72" height="72" fill="white"/>
</clipPath>
</defs>
</svg>
`

View File

@@ -0,0 +1,229 @@
package emergencykit
// NOTE:
// To view this file comfortably, disable line-wrapping in your editor.
const css = `
@page {
size: auto;
margin: 0mm; /* remove margin in printer settings */
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
margin-block-start: 0;
margin-block-end: 0;
font-family: -apple-system, Roboto;
}
body {
width: 480px;
}
strong {
font-weight: 500;
}
header {
display: flex;
justify-content: space-between;
padding: 20px 16px;
background-color: #F7FBFF;
}
h1 {
margin: 0;
font-family: -apple-system, Roboto;
font-weight: 500;
font-size: 24px;
line-height: 30px;
color: #182449;
}
h2 {
color: #182449;
}
h3 {
color: #182449;
}
header h2 {
margin: 3px 0 0 0;
font-family: Menlo, Roboto Mono;
font-weight: 500;
font-size: 18px;
line-height: 27px;
color: #576580;
text-transform: uppercase;
}
a {
color: #337cd0;
}
.backup {
margin: 24px 16px 0 16px;
background-color: #DFECFB;
}
.backup .intro {
display: flex;
padding: 16px 16px 16px 0;
}
.backup h1 {
margin-top: 16px;
margin-bottom: 8px;
font-weight: 500;
font-size: 32px;
line-height: 32px;
line-height: 32px;
}
.backup h2 {
font-size: 15px;
font-weight: 400;
font-size: 15px;
line-height: 24px;
color: #57656F;
}
.backup h2 strong {
color: #182449;
}
.backup h3 {
margin-bottom: 8px;
font-weight: 500;
font-size: 18px;
line-height: 24px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.backup svg {
margin: 8px 4px 0;
}
.backup .key {
padding: 32px 16px 24px 16px;
margin: 0 4px 4px 4px;
background-color: white;
}
.backup .key p {
font-family: Menlo, Roboto Mono;
font-weight: 400;
font-size: 13px;
word-wrap: break-word;
line-height: 24px;
color: #57656F;
}
.backup .date {
padding: 12px 0;
font-size: 13px;
line-height: 24px;
text-align: center;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #57656F;
}
.backup .date date {
font-weight: 500;
color: #182449;
}
section {
margin-top: 40px;
padding: 16px;
}
section h2 {
margin-top: 24px;
font-weight: 500;
font-size: 20px;
line-height: 32px;
}
section h3 {
font-weight: 500;
font-size: 20px;
line-height: 32px;
color: #182449;
}
section p {
margin-top: 8px;
font-weight: normal;
font-size: 16px;
line-height: 24px;
color: #576580;
}
.instructions .item {
display: flex;
margin-top: 32px;
}
.instructions .item .number-box {
margin-right: 16px;
padding-top: 6px;
}
.instructions .item .number {
width: 20px;
border-radius: 50%;
text-align: center;
font-weight: 500;
font-size: 14px;
line-height: 20px;
background: #2474CD;
color: #ffffff;
}
.help {
display: flex;
padding: 32px 16px 32px 0;
background: #F6F9FF;
}
.help svg {
margin: 0 8px;
}
.descriptors {
margin: 16px 0;
padding: 16px;
font-family: Menlo, Roboto Mono;
font-weight: 500;
font-size: 9px;
line-height: 240%;
letter-spacing: 1px;
list-style-type: none;
background: #F6F9FF;
color: #576580;
}
.descriptors .f {
color: #447BEF;
}
.descriptors .fp {
color: #d74a41;
}
.descriptors .checksum {
color: #a42fa2;
}
@media print {
.page-break-before { page-break-before: always; }
}
`

View File

@@ -0,0 +1,175 @@
package emergencykit
import (
"fmt"
"strings"
)
type DescriptorsData struct {
FirstFingerprint string
SecondFingerprint string
}
// Output descriptors shown in the PDF do not include legacy descriptors no longer in use. We leave
// the decision of whether to scan them to the Recovery Tool.
var descriptorFormats = []string{
"sh(wsh(multi(2, %s/1'/1'/0/*, %s/1'/1'/0/*)))", // V3 change
"sh(wsh(multi(2, %s/1'/1'/1/*, %s/1'/1'/1/*)))", // V3 external
"wsh(multi(2, %s/1'/1'/0/*, %s/1'/1'/0/*))", // V4 change
"wsh(multi(2, %s/1'/1'/1/*, %s/1'/1'/1/*))", // V4 external
"tr(musig(%s/1'/1'/0/*, %s/1'/1'/0/*))", // V5 change
"tr(musig(%s/1'/1'/1/*, %s/1'/1'/1/*))", // V5 external
}
// GetDescriptors returns an array of raw output descriptors.
func GetDescriptors(data *DescriptorsData) []string {
var descriptors []string
for _, descriptorFormat := range descriptorFormats {
descriptor := fmt.Sprintf(descriptorFormat, data.FirstFingerprint, data.SecondFingerprint)
checksum := calculateChecksum(descriptor)
descriptors = append(descriptors, descriptor+"#"+checksum)
}
return descriptors
}
// GetDescriptorsHTML returns the HTML for the output descriptor list in the Emergency Kit.
func GetDescriptorsHTML(data *DescriptorsData) string {
descriptors := GetDescriptors(data)
var itemsHTML []string
for _, descriptor := range descriptors {
descriptor, checksum := splitChecksum(descriptor)
html := descriptor
// Replace script type expressions (parenthesis in match prevent replacing the "sh" in "wsh")
html = strings.ReplaceAll(html, "wsh(", renderScriptType("wsh")+"(")
html = strings.ReplaceAll(html, "sh(", renderScriptType("sh")+"(")
html = strings.ReplaceAll(html, "multi(", renderScriptType("multi")+"(")
html = strings.ReplaceAll(html, "tr(", renderScriptType("tr")+"(")
html = strings.ReplaceAll(html, "musig(", renderScriptType("musig")+"(")
// Replace fingerprint expressions:
html = strings.ReplaceAll(html, data.FirstFingerprint, renderFingerprint(data.FirstFingerprint))
html = strings.ReplaceAll(html, data.SecondFingerprint, renderFingerprint(data.SecondFingerprint))
// Add checksum and wrap everything:
html += renderChecksum(checksum)
html = renderItem(html)
itemsHTML = append(itemsHTML, html)
}
return renderList(itemsHTML)
}
func renderList(itemsHTML []string) string {
return fmt.Sprintf(`<ul class="descriptors">%s</ul>`, strings.Join(itemsHTML, "\n"))
}
func renderItem(innerHTML string) string {
return fmt.Sprintf(`<li>%s</li>`, innerHTML)
}
func renderScriptType(scriptType string) string {
return fmt.Sprintf(`<span class="f">%s</span>`, scriptType)
}
func renderFingerprint(fingerprint string) string {
return fmt.Sprintf(`<span class="fp">%s</span>`, fingerprint)
}
func renderChecksum(checksum string) string {
return fmt.Sprintf(`#<span class="checksum">%s</span>`, checksum)
}
func splitChecksum(descriptor string) (string, string) {
parts := strings.Split(descriptor, "#")
if len(parts) == 1 {
return parts[0], ""
}
return parts[0], parts[1]
}
// -------------------------------------------------------------------------------------------------
// WARNING:
// Below this point, you may find only fear and confusion.
// I translated the code for computing checksums from the original C++ in the bitcoind source,
// making a few adjustments for language differences. It's a specialized algorithm for the domain of
// output descriptors, and it uses the same primitives as the bech32 encoding.
var inputCharset = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "
var checksumCharset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
func calculateChecksum(desc string) string {
var c uint64 = 1
var cls int = 0
var clscount int = 0
for _, ch := range desc {
pos := strings.IndexRune(inputCharset, ch)
if pos == -1 {
return ""
}
c = polyMod(c, pos&31)
cls = cls*3 + (pos >> 5)
clscount++
if clscount == 3 {
c = polyMod(c, cls)
cls = 0
clscount = 0
}
}
if clscount > 0 {
c = polyMod(c, cls)
}
for i := 0; i < 8; i++ {
c = polyMod(c, 0)
}
c ^= 1
ret := make([]byte, 8)
for i := 0; i < 8; i++ {
ret[i] = checksumCharset[(c>>(5*(7-i)))&31]
}
return string(ret)
}
func polyMod(c uint64, intVal int) uint64 {
val := uint64(intVal)
c0 := c >> 35
c = ((c & 0x7ffffffff) << 5) ^ val
if c0&1 != 0 {
c ^= 0xf5dee51989
}
if c0&2 != 0 {
c ^= 0xa9fdca3312
}
if c0&4 != 0 {
c ^= 0x1bab10e32d
}
if c0&8 != 0 {
c ^= 0x3706b1677a
}
if c0&16 != 0 {
c ^= 0x644d626ffd
}
return c
}

View File

@@ -0,0 +1,34 @@
package emergencykit
import "testing"
func TestChecksum(t *testing.T) {
// These descriptors are in https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md and
// their expected checksums obtained via the `getdescriptorinfo` RPC endpoint. Note that, to
// reproduce these results, you need a mainnet Bitcoin node (HD key parsing fails otherwise).
testChecksum(t, "gn28ywm7", "pk(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "8fhd9pwu", "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)")
testChecksum(t, "8zl0zxma", "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)")
testChecksum(t, "qkrrc7je", "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))")
testChecksum(t, "lq9sf04s", "combo(0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)")
testChecksum(t, "2wtr0ej5", "sh(wsh(pkh(02e493dbf1c10d80f3581e4904930b1404cc6c13900ee0758474fa94abe8c4cd13)))")
testChecksum(t, "hzhjw406", "multi(1,022f8bde4d1a07209355b4a7250a5c5128e88b84bddc619ab7cba8d569b240efe4,025cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc)")
testChecksum(t, "y9zthqta", "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))")
testChecksum(t, "en3tu306", "wsh(multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb,03d01115d548e7561b15c38f004d734633687cf4419620095bc5b0f47070afe85a))")
testChecksum(t, "ks05yr6p", "sh(wsh(multi(1,03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8,03499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e4,02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e)))")
testChecksum(t, "qwx6n9lh", "sh(sortedmulti(2,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01))")
testChecksum(t, "axav5m0j", "pk(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8)")
testChecksum(t, "kczqajcv", "pkh(xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw/1/2)")
testChecksum(t, "ml40v0wf", "pkh([d34db33f/44'/0'/0']xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*)")
testChecksum(t, "t2zpj2eu", "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
testChecksum(t, "v66cvalc", "wsh(sortedmulti(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))")
}
func testChecksum(t *testing.T, expectedChecksum string, descriptor string) {
actualChecksum := calculateChecksum(descriptor)
if actualChecksum != expectedChecksum {
t.Errorf("Descriptor %s checksum was %s expecting %s", descriptor, actualChecksum, expectedChecksum)
}
}

View File

@@ -0,0 +1,157 @@
package emergencykit
import (
"bytes"
"crypto/sha256"
"fmt"
"strconv"
"text/template"
"time"
)
// Input struct to fill the PDF
type Input struct {
FirstEncryptedKey string
FirstFingerprint string
SecondEncryptedKey string
SecondFingerprint string
Version int
}
// Output with the html as string and the verification code
type Output struct {
HTML string
VerificationCode string
}
var spanishMonthNames = []string{
"Enero",
"Febrero",
"Marzo",
"Abril",
"Mayo",
"Junio",
"Julio",
"Agosto",
"Septiembre",
"Octubre",
"Noviembre",
"Diciembre",
}
// GenerateHTML returns the translated emergency kit html as a string along with the verification code.
func GenerateHTML(params *Input, lang string) (*Output, error) {
verificationCode := generateDeterministicCode(params)
// Render output descriptors:
var descriptors string
if params.hasFingerprints() {
descriptors = GetDescriptorsHTML(&DescriptorsData{
FirstFingerprint: params.FirstFingerprint,
SecondFingerprint: params.SecondFingerprint,
})
}
// Render page body:
content, err := render("EmergencyKitContent", lang, &contentData{
// Externally provided:
FirstEncryptedKey: params.FirstEncryptedKey,
SecondEncryptedKey: params.SecondEncryptedKey,
// Computed by us:
VerificationCode: verificationCode,
CurrentDate: formatDate(time.Now(), lang),
Descriptors: descriptors,
// Template pieces separated for reuse:
IconHelp: iconHelp,
IconPadlock: iconPadlock,
})
if err != nil {
return nil, fmt.Errorf("failed to render EmergencyKitContent template: %w", err)
}
// Render complete HTML page:
page, err := render("EmergencyKitPage", lang, &pageData{
Css: css,
Content: content,
})
if err != nil {
return nil, fmt.Errorf("failed to render EmergencyKitPage template: %w", err)
}
return &Output{
HTML: page,
VerificationCode: verificationCode,
}, nil
}
func formatDate(t time.Time, lang string) string {
if lang == "en" {
return t.Format("January 2, 2006")
} else {
// Golang has no i18n facilities, so we do our own formatting.
year, month, day := t.Date()
monthName := spanishMonthNames[month-1]
return fmt.Sprintf("%d de %s, %d", day, monthName, year)
}
}
func generateDeterministicCode(params *Input) string {
// NOTE:
// This function creates a stable verification code given the inputs to render the Emergency Kit. For now, the
// implementation relies exclusively on the SecondEncryptedKey, which is the Muun key. This is obviously not ideal,
// since we're both dropping part of the input and introducing the assumption that the Muun key will always be
// rendered second -- but it compensates for a problem with one of our clients that causes the user key serialization
// to be recreated each time the kit is rendered (making this deterministic approach useless).
// Create a deterministic serialization of the input:
inputMaterial := params.SecondEncryptedKey + strconv.Itoa(params.Version)
// Compute a cryptographically secure hash of the material (critical, these are keys):
inputHash := sha256.Sum256([]byte(inputMaterial))
// Extract a verification code from the hash (doesn't matter if we discard bytes):
var code string
for _, b := range inputHash[:6] {
code += strconv.Itoa(int(b) % 10)
}
return code
}
func render(name, language string, data interface{}) (string, error) {
tmpl, err := template.New(name).Parse(getContent(name, language))
if err != nil {
return "", err
}
var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
if err != nil {
return "", err
}
return buf.String(), nil
}
func getContent(name string, language string) string {
switch name {
case "EmergencyKitPage":
return page
case "EmergencyKitContent":
if language == "es" {
return contentES
}
return contentEN
default:
panic("could not find template with name: " + name)
}
}
func (i *Input) hasFingerprints() bool {
return i.FirstFingerprint != "" && i.SecondFingerprint != ""
}

View File

@@ -0,0 +1,105 @@
package emergencykit
import (
"strings"
"testing"
)
func TestGenerateHTML(t *testing.T) {
out, err := GenerateHTML(&Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
SecondEncryptedKey: "MySecondEncryptedKey",
}, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
}
func TestGenerateHTMLWithFingerprints(t *testing.T) {
data := &Input{
FirstEncryptedKey: "MyFirstEncryptedKey",
FirstFingerprint: "abababab",
SecondEncryptedKey: "MySecondEncryptedKey",
SecondFingerprint: "cdcdcdcd",
}
out, err := GenerateHTML(data, "en")
if err != nil {
t.Fatal(err)
}
if len(out.VerificationCode) != 6 {
t.Fatal("expected verification code to have length 6")
}
if !strings.Contains(out.HTML, out.VerificationCode) {
t.Fatal("expected output html to contain verification code")
}
if !strings.Contains(out.HTML, "MyFirstEncryptedKey") {
t.Fatal("expected output html to contain first encrypted key")
}
if !strings.Contains(out.HTML, "MySecondEncryptedKey") {
t.Fatal("expected output html to contain second encrypted key")
}
if !strings.Contains(out.HTML, `<ul class="descriptors">`) {
t.Fatal("expected output html to contain output descriptors")
}
if !strings.Contains(out.HTML, `<span class="f">wsh</span>`) {
t.Fatal("expected output html to contain output descriptor scripts")
}
if !strings.Contains(out.HTML, `<span class="f">musig</span>`) {
t.Fatal("expected output html to contain musig output descriptor scripts")
}
if !strings.Contains(out.HTML, data.FirstFingerprint) {
t.Fatal("expected output html to contain FirstFingerprint")
}
if !strings.Contains(out.HTML, data.SecondFingerprint) {
t.Fatal("expected output html to contain SecondFingerprint")
}
}
func TestGenerateDeterministicCode(t *testing.T) {
// Create a base Input, without version, which we'll set for each case below:
input := &Input{
FirstEncryptedKey: "foo",
SecondEncryptedKey: "bar",
}
// List our cases for each version:
versionExpectedCodes := []struct {
version int
expectedCode string
}{
{1, "190981"},
{2, "257250"},
{3, "494327"},
}
// Do the thing:
for _, testCase := range versionExpectedCodes {
input.Version = testCase.version
code := generateDeterministicCode(input)
if code != testCase.expectedCode {
t.Fatalf("expected code from %+v to be %s, not %s", input, testCase.expectedCode, code)
}
}
}

View File

@@ -0,0 +1,145 @@
package emergencykit
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu"
)
// MetadataReader can extract the metadata file from a PDF.
type MetadataReader struct {
SrcFile string
}
// MetadataWriter can add the metadata file to a PDF.
type MetadataWriter struct {
SrcFile string
DstFile string
}
// Metadata holds the machine-readable data for an Emergency Kit.
type Metadata struct {
Version int `json:"version"`
BirthdayBlock int `json:"birthdayBlock"`
EncryptedKeys []*MetadataKey `json:"encryptedKeys"`
OutputDescriptors []string `json:"outputDescriptors"`
}
// MetadataKey holds an entry in the Metadata key array.
type MetadataKey struct {
DhPubKey string `json:"dhPubKey"`
EncryptedPrivKey string `json:"encryptedPrivKey"`
Salt string `json:"salt"`
}
// The name for the embedded metadata file in the PDF document:
const metadataName = "metadata.json"
// Default configuration values copied from pdfcpu source code (some values are irrelevant to us):
var pdfConfig = &pdfcpu.Configuration{
Reader15: true,
DecodeAllStreams: false,
ValidationMode: pdfcpu.ValidationRelaxed,
Eol: pdfcpu.EolLF,
WriteObjectStream: true,
WriteXRefStream: true,
EncryptUsingAES: true,
EncryptKeyLength: 256,
Permissions: pdfcpu.PermissionsNone,
}
// HasMetadata returns whether the metadata is present (and alone) in SrcFile.
func (mr *MetadataReader) HasMetadata() (bool, error) {
fs, err := api.ListAttachmentsFile(mr.SrcFile, pdfConfig)
if err != nil {
return false, fmt.Errorf("HasMetadata failed to list attachments: %w", err)
}
return len(fs) == 1 && fs[0] == metadataName, nil
}
// ReadMetadata returns the deserialized metadata file embedded in the SrcFile PDF.
func (mr *MetadataReader) ReadMetadata() (*Metadata, error) {
// NOTE:
// Due to library constraints, this makes use of a temporary directory in the default system temp
// location, which for the Recovery Tool will always be accessible. If we eventually want to read
// this metadata in mobile clients, we'll need the caller to provide a directory.
// Before we begin, verify that the metadata file is embedded:
hasMetadata, err := mr.HasMetadata()
if err != nil {
return nil, fmt.Errorf("ReadMetadata failed to check for existence: %w", err)
}
if !hasMetadata {
return nil, fmt.Errorf("ReadMetadata didn't find %s (or found more) in this PDF", metadataName)
}
// Create the temporary directory, with a deferred call to clean up:
tmpDir, err := ioutil.TempDir("", "ek-metadata-*")
if err != nil {
return nil, fmt.Errorf("ReadMetadata failed to create a temporary directory")
}
defer os.RemoveAll(tmpDir)
// Extract the embedded attachment from the PDF into that directory:
err = api.ExtractAttachmentsFile(mr.SrcFile, tmpDir, []string{metadataName}, pdfConfig)
if err != nil {
return nil, fmt.Errorf("ReadMetadata failed to extract attachment: %w", err)
}
// Read the contents of the file:
metadataBytes, err := ioutil.ReadFile(filepath.Join(tmpDir, metadataName))
if err != nil {
return nil, fmt.Errorf("ReadMetadata failed to read the extracted file: %w", err)
}
// Deserialize the metadata:
var metadata Metadata
err = json.Unmarshal(metadataBytes, &metadata)
if err != nil {
return nil, fmt.Errorf("ReadMetadata failed to unmarshal %s: %w", string(metadataBytes), err)
}
// Done we are!
return &metadata, nil
}
// WriteMetadata creates a copy of SrcFile with attached JSON metadata into DstFile.
func (mw *MetadataWriter) WriteMetadata(metadata *Metadata) error {
// NOTE:
// Due to library constraints, this makes use of a temporary file placed in the same directory as
// `SrcFile`, which is assumed to be writable. This is a much safer bet than attempting to pick a
// location for temporary files ourselves.
// Decide the location of the temporary file:
srcDir := filepath.Dir(mw.SrcFile)
tmpFile := filepath.Join(srcDir, metadataName)
// Serialize the metadata:
metadataBytes, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("WriteMetadata failed to marshal: %w", err)
}
// Write to the temporary file, with a deferred call to clean up:
err = ioutil.WriteFile(tmpFile, metadataBytes, os.FileMode(0600))
if err != nil {
return fmt.Errorf("WriteMetadata failed to write a temporary file: %w", err)
}
defer os.Remove(tmpFile)
// Add the attachment, returning potential errors:
err = api.AddAttachmentsFile(mw.SrcFile, mw.DstFile, []string{tmpFile}, false, pdfConfig)
if err != nil {
return fmt.Errorf("WriteMetadata failed to add attachment file %s: %w", tmpFile, err)
}
return nil
}

View File

@@ -0,0 +1,121 @@
package emergencykit
import (
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
var someMetadata = Metadata{
Version: 1,
BirthdayBlock: 12345,
EncryptedKeys: []*MetadataKey{
&MetadataKey{
DhPubKey: "0338c52ecbb886ab45de31120c76888da73437e3d6e81510f56d3746399f0fef52",
EncryptedPrivKey: "d0a801c1923663295892e9a9a0bfc770abcb00c20e7cef28e2d743c96b441e677c875e8d6495afb8362aba886ae9ee346c62e82758f5b5ba9a70f61957529255",
Salt: "d579c14c61365bc0",
},
},
OutputDescriptors: []string{
"sh(wsh(multi(2, 89a1749c/1'/1'/0/*, 77e21d45/1'/1'/0/*)))#0wp4hp36",
},
}
func TestReadWriteMetadata(t *testing.T) {
// Create a temporary directory and pick some suitable paths for our input/output files:
tmpDir := createTmpDir(t)
srcFile := filepath.Join(tmpDir, "src.pdf")
dstFile := filepath.Join(tmpDir, "src.pdf")
defer os.RemoveAll(tmpDir)
// Save the sample PDF (included at the end of this file, for readability):
createPdfFile(t, srcFile)
// Write metadata:
mw := MetadataWriter{
SrcFile: srcFile,
DstFile: dstFile,
}
mw.WriteMetadata(&someMetadata)
// Read metadata:
mr := MetadataReader{
SrcFile: dstFile,
}
metadata, err := mr.ReadMetadata()
if err != nil {
t.Fatalf("Failed to read metadata from %s: %v", dstFile, err)
}
// Verify that we got the original metadata back:
if !reflect.DeepEqual(&someMetadata, metadata) {
t.Fatalf("Metadata objects don't match: %v (%v vs %v)", err, someMetadata, metadata)
}
}
func createTmpDir(t *testing.T) string {
tmpDir, err := ioutil.TempDir("", "pdf")
if err != nil {
t.Fatalf("Failed to create temporary directory %s: %v", tmpDir, err)
}
return tmpDir
}
func createPdfFile(t *testing.T, path string) {
content, err := hex.DecodeString(strings.Join(strings.Fields(verySmallPdf), ""))
if err != nil {
t.Fatalf("Failed to hex-decode the sample PDF data: %v", err)
}
err = ioutil.WriteFile(path, content, os.FileMode(0600))
if err != nil {
t.Fatalf("Failed to write PDF to %s: %v", path, err)
}
}
// A very small valid PDF obtained by printing `<html></html>` with Chromium:
const verySmallPdf = `
255044462d312e340a25d3ebe9e10a312030206f626a0a3c3c2f43726561
746f7220284d6f7a696c6c612f352e30205c284d6163696e746f73683b20
496e74656c204d6163204f5320582031305f31345f365c29204170706c65
5765624b69742f3533372e3336205c284b48544d4c2c206c696b65204765
636b6f5c29204368726f6d652f38372e302e343238302e38382053616661
72692f3533372e3336290a2f50726f64756365722028536b69612f504446
206d3837290a2f4372656174696f6e446174652028443a32303230313231
313136333033332b303027303027290a2f4d6f64446174652028443a3230
3230313231313136333033332b303027303027293e3e0a656e646f626a0a
332030206f626a0a3c3c2f636120310a2f424d202f4e6f726d616c3e3e0a
656e646f626a0a342030206f626a0a3c3c2f46696c746572202f466c6174
654465636f64650a2f4c656e6774682039353e3e2073747265616d0a789c
d33332b60403050320d4d543e29a5b1a2924e772157281648c4c4d0d148c
8d0d0c148a52b9c2b514f280e2c67a8646a6607d08165083a1020806b92b
401845e95cfaeec60ae9c560732c0ccd140c0d4ccd40c6a471050221009d
2a19fb0a656e6473747265616d0a656e646f626a0a322030206f626a0a3c
3c2f54797065202f506167650a2f5265736f7572636573203c3c2f50726f
63536574205b2f504446202f54657874202f496d61676542202f496d6167
6543202f496d616765495d0a2f457874475374617465203c3c2f47332033
203020523e3e3e3e0a2f4d65646961426f78205b30203020363132203739
325d0a2f436f6e74656e74732034203020520a2f53747275637450617265
6e747320300a2f506172656e742035203020523e3e0a656e646f626a0a35
2030206f626a0a3c3c2f54797065202f50616765730a2f436f756e742031
0a2f4b696473205b32203020525d3e3e0a656e646f626a0a362030206f62
6a0a3c3c2f54797065202f436174616c6f670a2f50616765732035203020
523e3e0a656e646f626a0a787265660a3020370a30303030303030303030
2036353533352066200a30303030303030303135203030303030206e200a
30303030303030343731203030303030206e200a30303030303030323730
203030303030206e200a30303030303030333037203030303030206e200a
30303030303030363539203030303030206e200a30303030303030373134
203030303030206e200a747261696c65720a3c3c2f53697a6520370a2f52
6f6f742036203020520a2f496e666f2031203020523e3e0a737461727478
7265660a3736310a2525454f46
`