Skip to content

Instantly share code, notes, and snippets.

@thecb4
Last active February 13, 2021 13:27
Show Gist options
  • Save thecb4/6f8937d6f153e160572a00a189bc58a7 to your computer and use it in GitHub Desktop.
Save thecb4/6f8937d6f153e160572a00a189bc58a7 to your computer and use it in GitHub Desktop.
Swift for Vault
version: '3.8'
services:
vault:
image: vault
container_name: vault
environment:
VAULT_API_ADDR: 'http://0.0.0.0:8302'
ports:
- "8302:8302"
restart: always
volumes:
- ./volumes/logs:/vault/logs
- ./volumes/data:/vault/data
- ./volumes/config:/vault/config
cap_add:
- IPC_LOCK
entrypoint: vault server -config=/vault/config/vault.hcl
#!/usr/bin/env swift
import Foundation
import CryptoKit
extension Substring {
func trimmed() -> Substring {
guard let i = lastIndex(where: { $0 != " " }) else {
return ""
}
return self[...i]
}
}
public extension String {
func trimmingLines() -> String {
split(separator: "\n", omittingEmptySubsequences: false)
.map { $0.trimmed() }
.joined(separator: "\n")
}
func base64Decoded() -> String? {
if let data = Data(base64Encoded: self) {
return String(data: data, encoding: .utf8)
} else {
print("not base64 encoded")
}
return nil
}
}
extension Data {
func urlSafeBase64EncodedString() -> String {
return base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
}
}
struct Path {
private let path: String
init(_ path: String) {
self.path = path
}
func delete() throws {
try FileManager.default.removeItem(atPath: path)
}
func create() throws {
try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true)
}
}
@discardableResult
func executeCommand(
command: String,
cwd: URL? = nil, // To allow for testing of file based output
env: [String: String]? = nil
) throws -> String {
let splitCommand = command.split(separator: " ")
let arguments = splitCommand.dropFirst().map(String.init)
let commandName = String(splitCommand.first!)
let commandURL = URL(fileURLWithPath: commandName)
let process = Process()
if #available(macOS 10.13, *) {
process.executableURL = commandURL
} else {
process.launchPath = commandURL.path
}
process.arguments = arguments
if let workingDirectory = cwd {
process.currentDirectoryURL = workingDirectory
}
if let env = env {
process.environment = env
}
let output = Pipe()
process.standardOutput = output
let error = Pipe()
process.standardError = error
try process.run()
process.waitUntilExit()
let outputData = output.fileHandleForReading.readDataToEndOfFile()
let outputActual = String(data: outputData, encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)
let errorData = error.fileHandleForReading.readDataToEndOfFile()
let errorActual = String(data: errorData, encoding: .utf8)!
.trimmingCharacters(in: .whitespacesAndNewlines)
let finalString = errorActual + outputActual
return finalString
}
func sendPost(to urlString: String, with body: Data, handler: @escaping (Data?, URLResponse?, Error?) -> Void ) {
/* Configure session, choose between:
* defaultSessionConfiguration
* ephemeralSessionConfiguration
* backgroundSessionConfigurationWithIdentifier:
And set session-wide properties, such as: HTTPAdditionalHeaders,
HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest.
*/
let sessionConfig = URLSessionConfiguration.default
/* Create session, and optionally set a URLSessionDelegate. */
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
// Headers
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
// JSON Body
request.httpBody = body
print(request)
/* Start a new Task */
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
handler(data, response, error)
semaphore.signal()
})
task.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
}
func sendPost(to urlString: String, with body: Data, using token: String, handler: @escaping (Data?, URLResponse?, Error?) -> Void ) {
/* Configure session, choose between:
* defaultSessionConfiguration
* ephemeralSessionConfiguration
* backgroundSessionConfigurationWithIdentifier:
And set session-wide properties, such as: HTTPAdditionalHeaders,
HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest.
*/
let sessionConfig = URLSessionConfiguration.default
/* Create session, and optionally set a URLSessionDelegate. */
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "POST"
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
// Headers
request.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(token, forHTTPHeaderField: "X-Vault-Token")
// JSON Body
request.httpBody = body
print(request)
/* Start a new Task */
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
handler(data, response, error)
semaphore.signal()
})
task.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
}
func sendGet(to urlString: String, handler: @escaping (Data?, URLResponse?, Error?) -> Void ) {
/* Configure session, choose between:
* defaultSessionConfiguration
* ephemeralSessionConfiguration
* backgroundSessionConfigurationWithIdentifier:
And set session-wide properties, such as: HTTPAdditionalHeaders,
HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest.
*/
let sessionConfig = URLSessionConfiguration.default
/* Create session, and optionally set a URLSessionDelegate. */
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "GET"
// https://www.amarendrasingh.com/swift/urlsession-and-synchronous-http-request/
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
print(request)
/* Start a new Task */
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
handler(data, response, error)
semaphore.signal()
})
task.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
}
func sendGet(to urlString: String, using token: String, handler: @escaping (Data?, URLResponse?, Error?) -> Void ) {
/* Configure session, choose between:
* defaultSessionConfiguration
* ephemeralSessionConfiguration
* backgroundSessionConfigurationWithIdentifier:
And set session-wide properties, such as: HTTPAdditionalHeaders,
HTTPCookieAcceptPolicy, requestCachePolicy or timeoutIntervalForRequest.
*/
let sessionConfig = URLSessionConfiguration.default
/* Create session, and optionally set a URLSessionDelegate. */
let session = URLSession(configuration: sessionConfig, delegate: nil, delegateQueue: nil)
guard let url = URL(string: urlString) else {return}
var request = URLRequest(url: url)
request.httpMethod = "GET"
// https://www.amarendrasingh.com/swift/urlsession-and-synchronous-http-request/
let semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
// Headers
request.addValue(token, forHTTPHeaderField: "X-Vault-Token")
print(request)
/* Start a new Task */
let task = session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
handler(data, response, error)
semaphore.signal()
})
task.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
}
struct VaultInitializationStatus: Decodable {
let initialized: Bool
}
struct InitConfiguration: Encodable {
let secret_shares: Int
let secret_threshold: Int
}
struct SealConfiguration: Decodable {
let keys: [String]
let root_token: String
}
struct UnsealKey: Encodable {
let key: String
}
struct UnsealResponse: Decodable {
let type: String
let initialized: Bool
let sealed: Bool
let t: Int
let n: Int
let progress: Int
let nonce: String
let version: String
let migration: Bool
let cluster_name: String
let cluster_id: String
let recover_seal: Bool
let storage_type: String
}
struct SecretsConfiguration: Encodable {
let type: String
}
struct AuthenticationEnablement: Encodable {
struct Configuration: Encodable {
let default_lease_ttl: String
let max_lease_ttl: String
let force_no_cache: Bool
}
let type: String
let description: String
let config: AuthenticationEnablement.Configuration
let local: Bool
let seal_wrap: Bool
let external_entropy_access: Bool
}
struct JWTAuthConfiguration: Encodable {
let jwt_validation_pubkeys: [String]
let bound_issuer: String
let jwt_supported_algs: [String]
}
struct VaultPolicy: Encodable {
let name: String
let policy: String
}
struct JwtRole: Encodable {
let name: String
let role_type: String
let bound_audiences: [String]
let user_claim: String
let policies: [String]
let token_max_ttl: String
}
struct JWT: Codable {
struct Header: Codable {
let alg = "ES256"
let kid = "0001"
let typ = "JWT"
}
struct Payload: Codable {
let iss = "io.thecb4"
let iat = Date().timeIntervalSince1970
let exp = (Date() + 5 * 60).timeIntervalSince1970
let aud = "io.thecb4-v1"
let user_email: String
}
let header = Header()
let payload: Payload
init(user email: String) {
self.payload = Payload(user_email: email)
}
}
extension JWT {
func sign(using privateKey: P256.Signing.PrivateKey) -> String {
let headerJSONData = try! JSONEncoder().encode(header)
let headerBase64String = headerJSONData.urlSafeBase64EncodedString()
let payloadJSONData = try! JSONEncoder().encode(payload)
let payloadBase64String = payloadJSONData.urlSafeBase64EncodedString()
let toSign = (headerBase64String + "." + payloadBase64String).data(using: .utf8)!
let signature = try! privateKey.signature(for: toSign)
let rawSignature = signature.rawRepresentation
let signatureBase64String = Data(rawSignature).urlSafeBase64EncodedString()
if privateKey.publicKey.isValidSignature(signature, for: toSign) {
print("The signature is valid")
} else {
print("The signature is not valid")
}
let token = [headerBase64String, payloadBase64String, signatureBase64String].joined(separator: ".")
return token
}
}
struct JwtLogin: Encodable {
let role: String
let jwt: String
}
struct JwtAuthenticatedLogin: Decodable {
struct Auth: Decodable {
let client_token: String
let policies: [String]
let lease_duration: Int
}
let auth: Auth
}
func validateVaultInit(at urlPath: String) -> VaultInitializationStatus? {
var status: VaultInitializationStatus?
sendGet(to: vaultHost + "/v1/sys/init") { (data: Data?, response: URLResponse?, error: Error?) in
print("getting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
status = try? JSONDecoder().decode(VaultInitializationStatus.self, from: data)
print(str)
print(String(describing: sealConfiguration))
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
return status
}
func vaultInit(at urlPath: String, with configuration: InitConfiguration) -> SealConfiguration? {
var sealConfiguration: SealConfiguration?
guard let body = try? JSONEncoder().encode(configuration) else { return nil }
sendPost(to: vaultHost + "/v1/sys/init", with: body) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
sealConfiguration = try? JSONDecoder().decode(SealConfiguration.self, from: data)
print(str)
print(String(describing: sealConfiguration))
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
return sealConfiguration
}
func vaultUnseal(at urlPath: String, with unsealKey: UnsealKey, using token: String) -> UnsealResponse? {
var unsealResponse: UnsealResponse?
guard let body = try? JSONEncoder().encode(unsealKey) else { return nil }
sendPost(to: vaultHost + "/v1/sys/unseal", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
unsealResponse = try? JSONDecoder().decode(UnsealResponse.self, from: data)
print(str)
print(String(describing: unsealResponse))
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
return unsealResponse
}
// should return 204
func mountSecrets(configuration: SecretsConfiguration, using token: String) {
guard let body = try? JSONEncoder().encode(configuration) else { return }
sendPost(to: vaultHost + "/v1/sys/mounts/secret", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
// should return 204
func createSecret(on path: String, value: Data, using token: String) {
// guard let body = try? JSONEncoder().encode(configuration) else { return }
sendPost(to: vaultHost + "/v1/\(path)", with: value, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("created secret")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
func readSecret(on path: String, using token: String) -> String? {
var secret: String?
sendGet(to: vaultHost + "/v1/\(path)", using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("received secret")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
secret = String(decoding: data, as: UTF8.self)
print(secret)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
return secret
}
// should return 204
func vaultEnableAuth(for authentication: AuthenticationEnablement, using token: String) {
guard let body = try? JSONEncoder().encode(authentication) else { return }
sendPost(to: vaultHost + "/v1/sys/auth/jwt", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
// should return 204
func vaultConfigureJwtAuth(with configuration: JWTAuthConfiguration, using token: String) {
guard let body = try? JSONEncoder().encode(configuration) else { return }
sendPost(to: vaultHost + "/v1/auth/jwt/config", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
func vaultCreatePolicy(_ policy: VaultPolicy, using token: String) {
guard let body = try? JSONEncoder().encode(policy) else { return }
sendPost(to: vaultHost + "/v1/sys/policies/acl/\(policy.name)", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
func vaultCreateJwtRole(_ role: JwtRole, using token: String) {
guard let body = try? JSONEncoder().encode(role) else { return }
sendPost(to: vaultHost + "/v1/auth/jwt/role/\(role.name)", with: body, using: token) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
print(str)
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
}
func vaultJwtLogin(_ login: JwtLogin) -> JwtAuthenticatedLogin? {
guard let body = try? JSONEncoder().encode(login) else { return nil }
var authenticatedLogin: JwtAuthenticatedLogin?
sendPost(to: vaultHost + "/v1/auth/jwt/login", with: body) { (data: Data?, response: URLResponse?, error: Error?) in
print("posting data")
if (error == nil) {
// Success
let statusCode = (response as! HTTPURLResponse).statusCode
print("URL Session Task Succeeded: HTTP \(statusCode)")
if let data = data, let response = response {
let str = String(decoding: data, as: UTF8.self)
authenticatedLogin = try? JSONDecoder().decode(JwtAuthenticatedLogin.self, from: data)
print(str)
print(String(describing: authenticatedLogin))
print(response)
}
}
else {
// Failure
print("URL Session Task Failed: %@", error!.localizedDescription);
}
}
return authenticatedLogin
}
let path = ProcessInfo.processInfo.environment["PATH"]!
var environment = ["PATH": path]
let vaultConfig =
"""
ui = true
storage "file" {
path = "/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8302"
tls_disable = "true"
}
"""
do{ try Path("volumes/config").delete() } catch {}
do{ try Path("volumes/data").delete() } catch {}
do{ try Path("volumes/logs").delete() } catch {}
do{ try Path("volumes/config").create() } catch {}
do{ try Path("volumes/data").create() } catch {}
do{ try Path("volumes/logs").create() } catch {}
try vaultConfig.write(toFile: "volumes/config/vault.hcl", atomically: true, encoding: .utf8)
print("Starting vault")
let up = try executeCommand(command: "/usr/local/bin/docker-compose up -d --remove-orphans --force-recreate --always-recreate-deps --renew-anon-volumes", env: environment)
print(up)
let vaultHost = "http://0.0.0.0:8302"
var status = validateVaultInit(at: vaultHost)
let initConfiguration = InitConfiguration(secret_shares: 6, secret_threshold: 3)
let sealConfiguration = vaultInit(at: vaultHost, with: initConfiguration)
let unsealResponse1 = vaultUnseal(at: vaultHost, with: UnsealKey(key: sealConfiguration!.keys[0]), using: sealConfiguration!.root_token)
let unsealResponse2 = vaultUnseal(at: vaultHost, with: UnsealKey(key: sealConfiguration!.keys[1]), using: sealConfiguration!.root_token)
let unsealResponse3 = vaultUnseal(at: vaultHost, with: UnsealKey(key: sealConfiguration!.keys[2]), using: sealConfiguration!.root_token)
status = validateVaultInit(at: vaultHost)
let secretsConfiguration = SecretsConfiguration(type: "kv-v1")
mountSecrets(configuration: secretsConfiguration, using: sealConfiguration!.root_token)
let jwtEnablement = AuthenticationEnablement(
type: "jwt",
description: "",
config: AuthenticationEnablement.Configuration(
default_lease_ttl: "0s",
max_lease_ttl: "0s",
force_no_cache: false
),
local: false,
seal_wrap: false,
external_entropy_access: false
)
vaultEnableAuth(for: jwtEnablement, using: sealConfiguration!.root_token)
let privateKey = P256.Signing.PrivateKey()
let jwtAuthConfiguration = JWTAuthConfiguration(
jwt_validation_pubkeys: [privateKey.publicKey.pemRepresentation],
bound_issuer: "io.thecb4",
jwt_supported_algs: ["ES256"]
)
vaultConfigureJwtAuth(with: jwtAuthConfiguration, using: sealConfiguration!.root_token)
let appPolicy = VaultPolicy(
name: "app",
policy:
"""
path "secret/app/*" {
capabilities = ["create", "read"]
}
"""
)
vaultCreatePolicy(appPolicy, using: sealConfiguration!.root_token)
let jwtRole = JwtRole(
name: "jwtRole",
role_type: "jwt",
bound_audiences: ["io.thecb4-v1"],
user_claim: "user_email",
policies: ["app"],
token_max_ttl: "0s"
)
vaultCreateJwtRole(jwtRole, using: sealConfiguration!.root_token)
struct Header: Encodable {
let alg = "ES256"
let kid = "0001"
let typ = "JWT"
}
struct Payload: Encodable {
let iss = "io.thecb4"
let iat = Date().timeIntervalSince1970
let exp = (Date() + 5 * 60).timeIntervalSince1970
let aud = "io.thecb4-v1"
let user_email = "cavelle@tehcb4.io"
}
let jwtToken = JWT(user: "cavelle@tehcb4.io").sign(using: privateKey)
let jwtLogin = JwtLogin(
role: "jwtRole",
jwt: JWT(user: "cavelle@tehcb4.io").sign(using: privateKey)
)
let authenticated = vaultJwtLogin(jwtLogin)
print("your jwt login token is \(authenticated?.auth.client_token ?? "invalid")")
let secret =
"""
{"password": "my-long-password"}
""".data(using: .utf8)!
createSecret(on: "secret/app/info", value: secret, using: authenticated!.auth.client_token)
if let retrievedSecret = readSecret(on: "secret/app/info", using: authenticated!.auth.client_token) {
print(retrievedSecret)
} else {
print("Couldn't retrieve secret")
}
let down = try executeCommand(command: "/usr/local/bin/docker-compose down")
print(down)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment