0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-20 03:52:17 +02:00

bridge for username links

This commit is contained in:
Sergey Skrobotov 2023-07-06 14:39:37 -07:00
parent e50bec648f
commit ca262db5ec
24 changed files with 400 additions and 14 deletions

View File

@ -5,6 +5,9 @@
package org.signal.libsignal.usernames;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import junit.framework.TestCase;
import org.signal.libsignal.protocol.util.Hex;
@ -94,4 +97,44 @@ public class UsernamesTest extends TestCase {
}
}
public void testUsernameLinkHappyCase() throws BaseUsernameException {
final Username expectedUsername = new Username("hello_signal.42");
final Username.UsernameLink link = expectedUsername.generateLink();
final Username actualUsername = Username.fromLink(link);
assertEquals(expectedUsername.getUsername(), actualUsername.getUsername());
}
public void testCreateLinkFailsForLongUsername() throws BaseUsernameException {
final String longUsername = Stream.generate(() -> "a")
.limit(128)
.collect(Collectors.joining());
try {
new Username(longUsername).generateLink();
fail("Expected to fail creating a link for a long username");
} catch (BaseUsernameException ex) {
// this is fine
}
}
public void testDecryptUsernameFromLinkFailsForInvalidEntropySize() throws BaseUsernameException {
final byte[] entropy = new byte[16];
final byte[] encryptedUsername = new byte[32];
try {
Username.fromLink(new Username.UsernameLink(entropy, encryptedUsername));
fail("Expected to fail decrypting username link with an invalid entropy size");
} catch (BaseUsernameException ex) {
// this is fine
}
}
public void testDecryptUsernameFromLinkFailsForInvalidEncryptedUsername() throws BaseUsernameException {
final byte[] entropy = new byte[32];
final byte[] encryptedUsername = new byte[32];
try {
Username.fromLink(new Username.UsernameLink(entropy, encryptedUsername));
fail("Expected to fail decrypting username link with an invalid link data");
} catch (BaseUsernameException ex) {
// this is fine
}
}
}

View File

@ -523,6 +523,9 @@ public final class Native {
public static native byte[] UnidentifiedSenderMessageContent_GetSerialized(long obj);
public static native long UnidentifiedSenderMessageContent_New(CiphertextMessage message, long sender, int contentHint, byte[] groupId);
public static native byte[] UsernameLink_Create(String username);
public static native String UsernameLink_DecryptUsername(byte[] entropy, byte[] encryptedUsername);
public static native String Username_CandidatesFrom(String nickname, int minLen, int maxLen);
public static native byte[] Username_Hash(String username);
public static native byte[] Username_Proof(String username, byte[] randomness);

View File

@ -4,6 +4,8 @@
//
package org.signal.libsignal.usernames;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
import org.signal.libsignal.internal.Native;
import java.security.SecureRandom;
@ -12,16 +14,50 @@ import java.util.Arrays;
import java.util.List;
public final class Username {
private String value;
private byte[] hash;
private final String username;
private final byte[] hash;
public static class UsernameLink {
private final byte[] entropy;
private final byte[] encryptedUsername;
public UsernameLink(final byte[] entropy, final byte[] encryptedUsername) {
this.entropy = Objects.requireNonNull(entropy, "entropy");
this.encryptedUsername = Objects.requireNonNull(encryptedUsername, "encryptedUsername");
}
public byte[] getEntropy() {
return entropy;
}
public byte[] getEncryptedUsername() {
return encryptedUsername;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final UsernameLink that = (UsernameLink) o;
return Arrays.equals(entropy, that.entropy) &&
Arrays.equals(encryptedUsername, that.encryptedUsername);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(entropy);
result = 31 * result + Arrays.hashCode(encryptedUsername);
return result;
}
}
public Username(String username) throws BaseUsernameException {
this.value = username;
this.username = Objects.requireNonNull(username, "username");
this.hash = hash(username);
}
public String getUsername() {
return this.value;
return this.username;
}
public byte[] getHash() {
@ -37,6 +73,11 @@ public final class Username {
return result;
}
public static Username fromLink(final UsernameLink usernameLink) throws BaseUsernameException {
final String username = Native.UsernameLink_DecryptUsername(usernameLink.getEntropy(), usernameLink.getEncryptedUsername());
return new Username(username);
}
public byte[] generateProof() throws BaseUsernameException {
byte[] randomness = new byte[32];
SecureRandom r = new SecureRandom();
@ -45,7 +86,14 @@ public final class Username {
}
public byte[] generateProofWithRandomness(byte[] randomness) throws BaseUsernameException {
return Native.Username_Proof(this.value, randomness);
return Native.Username_Proof(this.username, randomness);
}
public UsernameLink generateLink() throws BaseUsernameException {
final byte[] bytes = Native.UsernameLink_Create(username);
final byte[] entropy = Arrays.copyOfRange(bytes, 0, 32);
final byte[] enctyptedUsername = Arrays.copyOfRange(bytes, 32, bytes.length);
return new UsernameLink(entropy, enctyptedUsername);
}
@Deprecated
@ -70,6 +118,19 @@ public final class Username {
@Override
public String toString() {
return this.value;
return this.username;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final Username username1 = (Username) o;
return username.equals(username1.username);
}
@Override
public int hashCode() {
return username.hashCode();
}
}

View File

@ -0,0 +1,7 @@
package org.signal.libsignal.usernames;
public class UsernameLinkInputDataTooLong extends BaseUsernameException {
public UsernameLinkInputDataTooLong(final String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package org.signal.libsignal.usernames;
public class UsernameLinkInvalidEntropyDataLength extends BaseUsernameException {
public UsernameLinkInvalidEntropyDataLength(final String message) {
super(message);
}
}

View File

@ -0,0 +1,7 @@
package org.signal.libsignal.usernames;
public class UsernameLinkInvalidLinkData extends BaseUsernameException {
public UsernameLinkInvalidLinkData(final String message) {
super(message);
}
}

2
node/Native.d.ts vendored
View File

@ -345,6 +345,8 @@ export function UnidentifiedSenderMessageContent_GetMsgType(m: Wrapper<Unidentif
export function UnidentifiedSenderMessageContent_GetSenderCert(m: Wrapper<UnidentifiedSenderMessageContent>): SenderCertificate;
export function UnidentifiedSenderMessageContent_New(message: Wrapper<CiphertextMessage>, sender: Wrapper<SenderCertificate>, contentHint: number, groupId: Buffer | null): UnidentifiedSenderMessageContent;
export function UnidentifiedSenderMessageContent_Serialize(obj: Wrapper<UnidentifiedSenderMessageContent>): Buffer;
export function UsernameLink_Create(username: string): Buffer;
export function UsernameLink_DecryptUsername(entropy: Buffer, encryptedUsername: Buffer): string;
export function Username_CandidatesFrom(nickname: string, minLen: number, maxLen: number): string;
export function Username_Hash(username: string): Buffer;
export function Username_Proof(username: string, randomness: Buffer): Buffer;

View File

@ -28,6 +28,10 @@ export enum ErrorCode {
InvalidMediaInput,
UnsupportedMediaInput,
InputDataTooLong,
InvalidEntropyDataLength,
InvalidUsernameLinkEncryptedData,
}
export class LibSignalErrorBase extends Error {
@ -135,6 +139,18 @@ export type NicknameTooLongError = LibSignalErrorCommon & {
code: ErrorCode.NicknameTooLong;
};
export type InputDataTooLong = LibSignalErrorCommon & {
code: ErrorCode.InputDataTooLong;
};
export type InvalidEntropyDataLength = LibSignalErrorCommon & {
code: ErrorCode.InvalidEntropyDataLength;
};
export type InvalidUsernameLinkEncryptedData = LibSignalErrorCommon & {
code: ErrorCode.InvalidUsernameLinkEncryptedData;
};
export type IoError = LibSignalErrorCommon & {
code: ErrorCode.IoError;
};
@ -162,6 +178,9 @@ export type LibSignalError =
| BadNicknameCharacterError
| NicknameTooShortError
| NicknameTooLongError
| InputDataTooLong
| InvalidEntropyDataLength
| InvalidUsernameLinkEncryptedData
| IoError
| InvalidMediaInputError
| UnsupportedMediaInputError;

View File

@ -7,6 +7,7 @@ import { assert, expect } from 'chai';
import { ErrorCode, LibSignalErrorBase } from '../Errors';
import * as usernames from '../usernames';
import * as util from './util';
import { UsernameLink } from '../usernames';
util.initLogger();
@ -84,4 +85,37 @@ describe('usernames', () => {
.with.property('code', ErrorCode.BadNicknameCharacter);
});
});
describe('link', () => {
it('works end to end with valid data', () => {
const expectedUsername = 'signal_test.42';
const usernameLinkData = usernames.createUsernameLink(expectedUsername);
const actualUsername = usernameLinkData.decryptUsername();
assert.equal(expectedUsername, actualUsername);
});
it('will error on too long input data', () => {
const longUsername = 'a'.repeat(128);
expect(() => usernames.createUsernameLink(longUsername))
.throws(LibSignalErrorBase)
.with.property('code', ErrorCode.InputDataTooLong);
});
it('will error on invalid entropy data size', () => {
const entropy = Buffer.alloc(16);
const encryptedUsername = Buffer.alloc(32);
expect(() =>
new UsernameLink(entropy, encryptedUsername).decryptUsername()
)
.throws(LibSignalErrorBase)
.with.property('code', ErrorCode.InvalidEntropyDataLength);
});
it('will error on invalid encrypted username data', () => {
const entropy = Buffer.alloc(32);
const encryptedUsername = Buffer.alloc(32);
expect(() =>
new UsernameLink(entropy, encryptedUsername).decryptUsername()
)
.throws(LibSignalErrorBase)
.with.property('code', ErrorCode.InvalidUsernameLinkEncryptedData);
});
});
});

View File

@ -37,6 +37,30 @@ export function generateProofWithRandom(
return Native.Username_Proof(username, random);
}
export class UsernameLink {
readonly _entropy: Buffer;
readonly _encryptedUsername: Buffer;
constructor(entropy: Buffer, encryptedUsername: Buffer) {
this._entropy = entropy;
this._encryptedUsername = encryptedUsername;
}
decryptUsername(): string {
return Native.UsernameLink_DecryptUsername(
this._entropy,
this._encryptedUsername
);
}
}
export function createUsernameLink(username: string): UsernameLink {
const usernameLinkData = Native.UsernameLink_Create(username);
const entropy = usernameLinkData.slice(0, 32);
const encryptedUsername = usernameLinkData.slice(32);
return new UsernameLink(entropy, encryptedUsername);
}
// Only for testing. Will throw on failure.
export function verifyProof(proof: Buffer, hash: Buffer): void {
Native.Username_Verify(proof, hash);

View File

@ -10,7 +10,7 @@ use libsignal_bridge::ffi::*;
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
use signal_pin::Error as PinError;
use usernames::UsernameError;
use usernames::{UsernameError, UsernameLinkError};
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
#[derive(Debug)]
@ -64,6 +64,9 @@ pub enum SignalErrorCode {
UsernameTooShort = 125,
UsernameTooLong = 126,
UsernameLinkInvalidEntropyDataLength = 127,
UsernameLinkInvalid = 128,
IoError = 130,
#[allow(dead_code)]
InvalidMediaInput = 131,
@ -231,7 +234,8 @@ impl From<&SignalFfiError> for SignalErrorCode {
SignalErrorCode::UsernameTooShort
}
SignalFfiError::UsernameError(UsernameError::NicknameTooLong) => {
SignalFfiError::UsernameError(UsernameError::NicknameTooLong)
| SignalFfiError::UsernameLinkError(UsernameLinkError::InputDataTooLong) => {
SignalErrorCode::UsernameTooLong
}
@ -239,6 +243,12 @@ impl From<&SignalFfiError> for SignalErrorCode {
SignalErrorCode::VerificationFailure
}
SignalFfiError::UsernameLinkError(UsernameLinkError::InvalidEntropyDataLength) => {
SignalErrorCode::UsernameLinkInvalidEntropyDataLength
}
SignalFfiError::UsernameLinkError(_) => SignalErrorCode::UsernameLinkInvalid,
SignalFfiError::Io(_) => SignalErrorCode::IoError,
#[cfg(feature = "signal-media")]

View File

@ -334,6 +334,13 @@ impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameError> {
}
}
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameLinkError> {
type ResultType = T::ResultType;
fn convert_into(self) -> SignalFfiResult<Self::ResultType> {
T::convert_into(self?)
}
}
/// Allocates and returns a new Rust-owned C string.
impl ResultTypeInfo for String {
type ResultType = *const libc::c_char;

View File

@ -13,7 +13,7 @@ use device_transfer::Error as DeviceTransferError;
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
use signal_pin::Error as PinError;
use usernames::UsernameError;
use usernames::{UsernameError, UsernameLinkError};
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
use crate::support::describe_panic;
@ -32,6 +32,7 @@ pub enum SignalFfiError {
ZkGroupVerificationFailure(ZkGroupVerificationFailure),
ZkGroupDeserializationFailure(ZkGroupDeserializationFailure),
UsernameError(UsernameError),
UsernameLinkError(UsernameLinkError),
Io(IoError),
#[cfg(feature = "signal-media")]
MediaSanitizeParse(signal_media::sanitize::ParseErrorReport),
@ -60,6 +61,7 @@ impl fmt::Display for SignalFfiError {
SignalFfiError::ZkGroupVerificationFailure(e) => write!(f, "{}", e),
SignalFfiError::ZkGroupDeserializationFailure(e) => write!(f, "{}", e),
SignalFfiError::UsernameError(e) => write!(f, "{}", e),
SignalFfiError::UsernameLinkError(e) => write!(f, "{}", e),
SignalFfiError::Io(e) => write!(f, "IO error: {}", e),
#[cfg(feature = "signal-media")]
SignalFfiError::MediaSanitizeParse(e) => {
@ -128,6 +130,12 @@ impl From<UsernameError> for SignalFfiError {
}
}
impl From<UsernameLinkError> for SignalFfiError {
fn from(e: UsernameLinkError) -> SignalFfiError {
SignalFfiError::UsernameLinkError(e)
}
}
impl From<IoError> for SignalFfiError {
fn from(e: IoError) -> SignalFfiError {
Self::Io(e)

View File

@ -832,6 +832,16 @@ impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameError> {
}
}
impl<T: ResultTypeInfo> ResultTypeInfo for Result<T, usernames::UsernameLinkError> {
type ResultType = T::ResultType;
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {
T::convert_into(self?, env)
}
fn convert_into_jobject(signal_jni_result: &SignalJniResult<Self::ResultType>) -> JObject {
<T as ResultTypeInfo>::convert_into_jobject(signal_jni_result)
}
}
impl<T: ResultTypeInfo> ResultTypeInfo for SignalJniResult<T> {
type ResultType = T::ResultType;
fn convert_into(self, env: &JNIEnv) -> SignalJniResult<Self::ResultType> {

View File

@ -13,7 +13,7 @@ use device_transfer::Error as DeviceTransferError;
use libsignal_protocol::*;
use signal_crypto::Error as SignalCryptoError;
use signal_pin::Error as PinError;
use usernames::UsernameError;
use usernames::{UsernameError, UsernameLinkError};
use zkgroup::{ZkGroupDeserializationFailure, ZkGroupVerificationFailure};
use crate::support::describe_panic;
@ -32,6 +32,7 @@ pub enum SignalJniError {
ZkGroupDeserializationFailure(ZkGroupDeserializationFailure),
ZkGroupVerificationFailure(ZkGroupVerificationFailure),
UsernameError(UsernameError),
UsernameLinkError(UsernameLinkError),
Io(IoError),
#[cfg(feature = "signal-media")]
MediaSanitizeParse(signal_media::sanitize::ParseErrorReport),
@ -59,6 +60,7 @@ impl fmt::Display for SignalJniError {
SignalJniError::ZkGroupVerificationFailure(e) => write!(f, "{}", e),
SignalJniError::ZkGroupDeserializationFailure(e) => write!(f, "{}", e),
SignalJniError::UsernameError(e) => write!(f, "{}", e),
SignalJniError::UsernameLinkError(e) => write!(f, "{}", e),
SignalJniError::Io(e) => write!(f, "{}", e),
#[cfg(feature = "signal-media")]
SignalJniError::MediaSanitizeParse(e) => write!(f, "{}", e),
@ -139,6 +141,12 @@ impl From<UsernameError> for SignalJniError {
}
}
impl From<UsernameLinkError> for SignalJniError {
fn from(e: UsernameLinkError) -> Self {
SignalJniError::UsernameLinkError(e)
}
}
impl From<IoError> for SignalJniError {
fn from(e: IoError) -> SignalJniError {
Self::Io(e)

View File

@ -39,7 +39,7 @@ pub use io::*;
mod storage;
pub use storage::*;
use usernames::UsernameError;
use usernames::{UsernameError, UsernameLinkError};
/// The type of boxed Rust values, as surfaced in JavaScript.
pub type ObjectHandle = jlong;
@ -403,6 +403,23 @@ fn throw_error(env: &JNIEnv, error: SignalJniError) {
)
}
SignalJniError::UsernameLinkError(UsernameLinkError::InputDataTooLong) => {
jni_class_name!(org.signal.libsignal.usernames.UsernameLinkInputDataTooLong)
}
SignalJniError::UsernameLinkError(UsernameLinkError::InvalidEntropyDataLength) => {
jni_class_name!(
org.signal
.libsignal
.usernames
.UsernameLinkInvalidEntropyDataLength
)
}
SignalJniError::UsernameLinkError(_) => {
jni_class_name!(org.signal.libsignal.usernames.UsernameLinkInvalidLinkData)
}
SignalJniError::Io(_) => {
jni_class_name!(java.io.IOException)
}

View File

@ -226,6 +226,32 @@ impl SignalNodeError for usernames::UsernameError {
}
}
impl SignalNodeError for usernames::UsernameLinkError {
fn throw<'a>(
self,
cx: &mut impl Context<'a>,
module: Handle<'a, JsObject>,
operation_name: &str,
) -> JsResult<'a, JsValue> {
let name = match &self {
Self::InputDataTooLong => Some("InputDataTooLong"),
Self::InvalidEntropyDataLength => Some("InvalidEntropyDataLength"),
Self::UsernameLinkDataTooShort
| Self::HmacMismatch
| Self::BadCiphertext
| Self::InvalidDecryptedDataStructure => Some("InvalidUsernameLinkEncryptedData"),
};
let message = self.to_string();
match new_js_error(cx, module, name, &message, operation_name, None) {
Some(error) => cx.throw(error),
None => {
// Make sure we still throw something.
cx.throw_error(message)
}
}
}
}
impl SignalNodeError for sanitize::Error {
fn throw<'a>(
self,

View File

@ -9,7 +9,11 @@ use libsignal_bridge_macros::*;
use crate::support::*;
use crate::*;
use ::usernames::{NicknameLimits, Username, UsernameError};
#[allow(unused_imports)]
use ::usernames::{
create_for_username, decrypt_username, NicknameLimits, Username, UsernameError,
UsernameLinkError,
};
#[bridge_fn]
pub fn Username_Hash(username: String) -> Result<[u8; 32], UsernameError> {
@ -41,3 +45,17 @@ pub fn Username_CandidatesFrom(
let limits = NicknameLimits::new(min_len as usize, max_len as usize);
Username::candidates_from(&mut rng, &nickname, limits).map(|names| names.join(","))
}
#[bridge_fn]
pub fn UsernameLink_Create(username: String) -> Result<Vec<u8>, UsernameLinkError> {
let mut rng = rand::rngs::OsRng;
create_for_username(&mut rng, username)
}
#[bridge_fn]
pub fn UsernameLink_DecryptUsername(
entropy: &[u8],
encrypted_username: &[u8],
) -> Result<String, UsernameLinkError> {
decrypt_username(entropy, encrypted_username)
}

View File

@ -27,10 +27,10 @@ pub enum UsernameError {
pub enum UsernameLinkError {
/// The combined length of all input data is too long
InputDataTooLong,
/// Username link data size is too short: must contain IV, ciphertext, and HMAC
UsernameLinkDataTooShort,
/// Invalid size of the entropy data
InvalidEntropyDataLength,
/// Username link data size is too short: must contain IV, ciphertext, and HMAC
UsernameLinkDataTooShort,
/// HMAC on username link doesn't match the one calculated with the given entropy input
HmacMismatch,
/// Ciphertext in the username link can't be decrypted

View File

@ -322,6 +322,7 @@ mod test {
Username::new(username).map(|name| name.hash()).unwrap();
}
}
#[test]
fn invalid_usernames() {
for username in [

View File

@ -42,6 +42,8 @@ public enum SignalError: Error {
case badNicknameCharacter(String)
case nicknameTooShort(String)
case nicknameTooLong(String)
case usernameLinkInvalidEntropyDataLength(String)
case usernameLinkInvalid(String)
case ioError(String)
case invalidMediaInput(String)
case unsupportedMediaInput(String)
@ -130,6 +132,10 @@ internal func checkError(_ error: SignalFfiErrorRef?) throws {
throw SignalError.nicknameTooShort(errStr)
case SignalErrorCodeUsernameTooLong:
throw SignalError.nicknameTooLong(errStr)
case SignalErrorCodeUsernameLinkInvalidEntropyDataLength:
throw SignalError.usernameLinkInvalidEntropyDataLength(errStr)
case SignalErrorCodeUsernameLinkInvalid:
throw SignalError.usernameLinkInvalid(errStr)
case SignalErrorCodeIoError:
throw SignalError.ioError(errStr)
case SignalErrorCodeInvalidMediaInput:

View File

@ -15,6 +15,24 @@ public struct Username {
self.hash = try generateHash(self.value)
}
public init<
LinkBytes: ContiguousBytes,
RandBytes: ContiguousBytes
>(
fromLink bytes: LinkBytes,
withRandomness randomness: RandBytes
) throws {
let username =
try randomness.withUnsafeBorrowedBuffer { randBuffer in
try bytes.withUnsafeBorrowedBuffer { bytesBuffer in
try invokeFnReturningString {
signal_username_link_decrypt_username($0, randBuffer, bytesBuffer)
}
}
}
try self.init(username)
}
public func generateProof(withRandomness randomness: Randomness? = nil) -> [UInt8] {
failOnError {
let randomness = try randomness ?? Randomness.generate()
@ -30,6 +48,17 @@ public struct Username {
}
}
public func createLink() throws -> ([UInt8], [UInt8]) {
let bytes = failOnError {
return try self.value.withCString { usernamePtr in
try invokeFnReturningArray {
signal_username_link_create($0, usernamePtr)
}
}
}
return (Array(bytes[..<32]), Array(bytes[32...]))
}
public static func verify(proof: [UInt8], forHash hash: [UInt8]) throws {
try checkError(
proof.withUnsafeBorrowedBuffer { proofPtr in
@ -59,6 +88,8 @@ extension Username: CustomStringConvertible {
}
}
extension Username: Equatable { }
private func generateHash(_ s: String) throws -> [UInt8] {
try s.withCString { strPtr in
try invokeFnReturningFixedLengthArray {

View File

@ -166,6 +166,8 @@ typedef enum {
SignalErrorCodeUsernameBadCharacter = 124,
SignalErrorCodeUsernameTooShort = 125,
SignalErrorCodeUsernameTooLong = 126,
SignalErrorCodeUsernameLinkInvalidEntropyDataLength = 127,
SignalErrorCodeUsernameLinkInvalid = 128,
SignalErrorCodeIoError = 130,
SignalErrorCodeInvalidMediaInput = 131,
SignalErrorCodeUnsupportedMediaInput = 132,
@ -1122,6 +1124,10 @@ SignalFfiError *signal_username_verify(SignalBorrowedBuffer proof, SignalBorrowe
SignalFfiError *signal_username_candidates_from(const char **out, const char *nickname, uint32_t min_len, uint32_t max_len);
SignalFfiError *signal_username_link_create(SignalOwnedBuffer *out, const char *username);
SignalFfiError *signal_username_link_decrypt_username(const char **out, SignalBorrowedBuffer entropy, SignalBorrowedBuffer encrypted_username);
#if defined(SIGNAL_MEDIA_SUPPORTED)
SignalFfiError *signal_signal_media_check_available(void);
#endif

View File

@ -78,4 +78,35 @@ class UsernameTests: TestCaseBase {
XCTFail("Unexpected error thrown")
}
}
func testUsernameLinkWorksEndToEnd() throws {
let original = try Username("SiGNAl.42")
let (randomness, linkBytes) = try original.createLink()
let recreated = try Username(fromLink: linkBytes, withRandomness: randomness)
XCTAssertEqual(original, recreated)
}
func testUsernameLinkInvalidEntropySize() throws {
do {
let randomness = [UInt8](repeating: 0, count: 16)
let linkBytes = [UInt8](repeating: 0, count: 32)
_ = try Username(fromLink: linkBytes, withRandomness: randomness)
XCTFail("Should have failed")
} catch SignalError.usernameLinkInvalidEntropyDataLength {
} catch {
XCTFail("Unexpected error thrown")
}
}
func testUsernameLinkInvalidLinkBytes() throws {
do {
let randomness = [UInt8](repeating: 0, count: 32)
let linkBytes = [UInt8](repeating: 0, count: 32)
_ = try Username(fromLink: linkBytes, withRandomness: randomness)
XCTFail("Should have failed")
} catch SignalError.usernameLinkInvalid {
} catch {
XCTFail("Unexpected error thrown")
}
}
}